#requires -Version 5.1 <# .SYNOPSIS Unified launcher for the macOS Intune Toolkit. .DESCRIPTION Presents a single terminal UI to choose from all available headless Intune management tools. Passes through common auth parameters. Press Esc to go back to the menu from any selection. .EXAMPLE ./Scripts/Start-IntuneToolkit.ps1 -TenantId "contoso.onmicrosoft.com" #> [CmdletBinding()] param( [string]$TenantId, [string]$AppId, [string]$Secret, [string]$Certificate, [ValidateSet("AppOnly","Browser","DeviceCode")] [string]$AuthMode = "AppOnly", [string]$RedirectUri, [string]$SettingsFile ) $ErrorActionPreference = "Stop" #region Helper functions function Test-FzfAvailable { return [bool](Get-Command fzf -ErrorAction SilentlyContinue) } function Show-FzfHint { if(Test-FzfAvailable) { return } Write-Host "`n[fzf not found]" -ForegroundColor Yellow -NoNewline Write-Host " Install fzf for the best interactive menu experience.`n" -ForegroundColor DarkGray if($IsMacOS) { Write-Host " macOS: brew install fzf" -ForegroundColor DarkGray } elseif($IsLinux) { Write-Host " Debian/Ubuntu: sudo apt install fzf" -ForegroundColor DarkGray Write-Host " Fedora: sudo dnf install fzf" -ForegroundColor DarkGray Write-Host " Arch: sudo pacman -S fzf" -ForegroundColor DarkGray } else { Write-Host " Windows: winget install junegunn.fzf" -ForegroundColor DarkGray Write-Host " choco install fzf" -ForegroundColor DarkGray } Write-Host " (Falling back to numbered menus for now.)`n" -ForegroundColor DarkGray } function Show-FzfMenu { param( [Parameter(Mandatory)] [string[]]$Items, [string]$Header = "Select one" ) $selected = $Items | fzf --header=$Header if(-not $selected) { return $null } return $selected } function Show-NumberedMenu { param( [Parameter(Mandatory)] [string[]]$Items, [string]$Header = "Select one" ) Write-Host "`n$Header" -ForegroundColor Cyan for($i=0; $i -lt $Items.Count; $i++) { Write-Host " $($i+1). $($Items[$i])" } $choice = Read-Host "Enter a number (0 to exit)" if($choice -eq "0") { return "EXIT" } $index = [int]$choice - 1 if($index -ge 0 -and $index -lt $Items.Count) { return $Items[$index] } return $null } function Select-MenuItem { param( [Parameter(Mandatory)] [string[]]$Items, [string]$Header = "Select one" ) if(Test-FzfAvailable) { return Show-FzfMenu -Items $Items -Header $Header } return Show-NumberedMenu -Items $Items -Header $Header } #endregion $projectRoot = Split-Path -Parent $PSScriptRoot #region Tenant selection function Get-DefaultSettingsPath { if($IsWindows -or $env:OS -eq "Windows_NT") { if($env:LOCALAPPDATA) { return (Join-Path $env:LOCALAPPDATA "macOS_IntuneManagement\Settings.json") } return (Join-Path $env:USERPROFILE "AppData\Local\macOS_IntuneManagement\Settings.json") } if($IsMacOS) { return (Join-Path $HOME "Library/Application Support/macOS_IntuneManagement/Settings.json") } return (Join-Path $HOME ".local/share/macOS_IntuneManagement/Settings.json") } function Get-SavedTenants { param([string]$SettingsPath) if(-not (Test-Path $SettingsPath)) { return @() } try { $raw = Get-Content $SettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json -AsHashtable -ErrorAction Stop $tenants = @() foreach($key in $raw.Keys) { if($key -match '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$') { $name = $null if($raw[$key] -is [hashtable] -and $raw[$key].ContainsKey('TenantName')) { $name = $raw[$key]['TenantName'] } elseif($raw[$key] -is [psobject] -and $raw[$key].PSObject.Properties['TenantName']) { $name = $raw[$key].TenantName } $display = if($name) { "$name ($key)" } else { $key } $tenants += [PSCustomObject]@{ TenantId = $key; TenantName = $name; Display = $display } } } return $tenants | Sort-Object Display } catch { return @() } } function Update-TenantNameCache { param([string]$SettingsPath, [string]$TenantId, [string]$TenantName) if(-not (Test-Path $SettingsPath)) { return } try { $raw = Get-Content $SettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json -AsHashtable -ErrorAction Stop if($raw[$TenantId] -is [hashtable]) { $raw[$TenantId]['TenantName'] = $TenantName } else { $raw[$TenantId] = @{ TenantName = $TenantName } } $raw | ConvertTo-Json -Depth 10 | Set-Content -Path $SettingsPath -Force } catch { } } function Resolve-TenantName { param([string]$TenantId, [string]$SettingsPath) $settingsObj = $null try { $settingsObj = Get-Content $SettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json -AsHashtable -ErrorAction Stop } catch { return $null } $tenantNode = $settingsObj[$TenantId] if(-not $tenantNode) { return $null } $appId = $tenantNode['GraphAzureAppId'] if(-not $appId) { return $null } $secret = $tenantNode['GraphAzureAppSecret'] $cert = $tenantNode['GraphAzureAppCert'] if(-not $secret -and $IsMacOS) { try { $keychainSecret = security find-generic-password -a "IntuneManagement" -s "IntuneMgmt-$AppId" -w 2>$null if($keychainSecret) { $secret = $keychainSecret } } catch { } } $runtimeModule = Join-Path $projectRoot "Runtime/IntuneManagement.Runtime.psd1" if(-not (Test-Path $runtimeModule)) { return $null } $invokeParams = @{ Silent = $true JSonSettings = $true JSonFile = $SettingsPath TenantId = $TenantId AppId = $appId AuthMode = "AppOnly" } if($secret) { $invokeParams.Secret = $secret } elseif($cert) { $invokeParams.Certificate = $cert } try { Import-Module $runtimeModule -Force | Out-Null Initialize-IntuneManagementRuntime -View "IntuneGraphAPI" @invokeParams | Out-Null if(Get-Command Invoke-GraphRequest -ErrorAction SilentlyContinue) { $org = Invoke-GraphRequest "/organization" -ErrorAction Stop if($org.value -and $org.value[0].displayName) { return $org.value[0].displayName } } } catch { } return $null } $settingsPath = $SettingsFile if(-not $settingsPath) { $settingsPath = Get-DefaultSettingsPath } if(-not $TenantId) { $tenants = Get-SavedTenants -SettingsPath $settingsPath $tenantOptions = @() foreach($t in $tenants) { $tenantOptions += $t.Display } $tenantOptions += "[+ Onboard new tenant]" $tenantOptions += "[Exit]" $selectedTenantDisplay = Select-MenuItem -Items $tenantOptions -Header "Select a tenant" if(-not $selectedTenantDisplay -or $selectedTenantDisplay -eq "[Exit]") { exit 0 } elseif($selectedTenantDisplay -eq "[+ Onboard new tenant]") { $TenantId = Read-Host "Enter the new Tenant ID (GUID)" if(-not $TenantId) { Write-Host "No tenant ID provided. Exiting." -ForegroundColor Yellow exit 0 } $initPath = Join-Path $projectRoot "Scripts/Initialize-IntuneAuth.ps1" & $initPath -TenantId $TenantId Write-Host "`nOnboarding complete. Restarting launcher..." -ForegroundColor Green Start-Sleep -Seconds 1 & $PSCommandPath exit 0 } else { $TenantId = $selectedTenantDisplay -replace '.*\(([0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})\)$', '$1' if(-not $TenantId) { $TenantId = $selectedTenantDisplay } } } $currentTenant = (Get-SavedTenants -SettingsPath $settingsPath) | Where-Object { $_.TenantId -eq $TenantId } | Select-Object -First 1 if(-not $currentTenant -or -not $currentTenant.TenantName) { Write-Host "`nResolving tenant name..." -ForegroundColor Cyan $resolvedName = Resolve-TenantName -TenantId $TenantId -SettingsPath $settingsPath if($resolvedName) { Update-TenantNameCache -SettingsPath $settingsPath -TenantId $TenantId -TenantName $resolvedName Write-Host "Cached tenant name: $resolvedName" -ForegroundColor Green $currentTenant = [PSCustomObject]@{ TenantId = $TenantId; TenantName = $resolvedName; Display = "$resolvedName ($TenantId)" } } } #endregion Show-FzfHint # Build common parameter hashtable $commonParams = @{ TenantId = $TenantId AppId = $AppId Secret = $Secret Certificate = $Certificate AuthMode = $AuthMode RedirectUri = $RedirectUri SettingsFile = $SettingsFile } $menuItems = @( "15. Delete tenant auth and app registration" "14. Delete local tenant auth only" "13. Refresh tenant names" "12. Initialize auth (one-time setup)" "11. Deploy baseline (dry-run / WhatIf)" "10. Deploy baseline" "9. Bulk device operations" "8. Bulk rename policies" "7. Export assignments to CSV/Markdown" "6. Restore assignments" "5. Backup assignments" "4. Bulk assignment manager (policies)" "3. Bulk app assignment" "2. Import policies" "1. Export policies" "0. Exit" ) while($true) { Clear-Host Write-Host "========================================" -ForegroundColor Cyan Write-Host " macOS Intune Toolkit" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan if($currentTenant -and $currentTenant.TenantName) { Write-Host " Tenant: $($currentTenant.TenantName) ($TenantId)" -ForegroundColor Green } else { Write-Host " Tenant: $TenantId" -ForegroundColor Green } Write-Host " Press Esc to go back, Space to select" -ForegroundColor DarkGray $selection = Select-MenuItem -Items $menuItems -Header "Select a tool to launch" if(-not $selection) { continue } if($selection -eq "EXIT" -or $selection -like "*0. Exit*") { Write-Host "`nExiting. Goodbye!" -ForegroundColor Yellow exit 0 } $choiceNumber = [int]($selection -replace "^(\d+)\..*$", '$1') switch($choiceNumber) { 1 { $script = "Start-HeadlessIntune.ps1"; $commonParams.Interactive = $true } 2 { $script = "Start-HeadlessIntune.ps1"; $commonParams.Interactive = $true } 3 { $script = "Scripts/Bulk-AppAssignment.ps1" } 4 { $script = "Scripts/Bulk-AssignmentManager.ps1" } 5 { $script = "Scripts/Backup-Restore-Assignments.ps1"; $commonParams.Mode = "Backup" } 6 { $script = "Scripts/Backup-Restore-Assignments.ps1"; $commonParams.Mode = "Restore" } 7 { $script = "Scripts/Export-AssignmentsToCsv.ps1" } 8 { $script = "Scripts/Bulk-RenamePolicies.ps1" } 9 { $script = "Scripts/Bulk-DeviceOperations.ps1" } 10 { $script = "Scripts/Deploy-IntuneBaseline.ps1" } 11 { $script = "Scripts/Deploy-IntuneBaseline.ps1"; $commonParams.WhatIf = $true } 12 { $script = "Scripts/Initialize-IntuneAuth.ps1" } 13 { $script = $null } 14 { $script = "Scripts/Initialize-IntuneAuth.ps1" } 15 { $script = "Scripts/Initialize-IntuneAuth.ps1" } default { continue } } # Clear any mode-specific params from previous loop iteration $commonParams.Remove("Interactive") $commonParams.Remove("Mode") $commonParams.Remove("WhatIf") switch($choiceNumber) { 1 { $commonParams.Interactive = $true } 2 { $commonParams.Interactive = $true } 5 { $commonParams.Mode = "Backup" } 6 { $commonParams.Mode = "Restore" } 11 { $commonParams.WhatIf = $true } } if($choiceNumber -eq 13) { Write-Host "`nRefreshing tenant names..." -ForegroundColor Cyan $tenantsToRefresh = Get-SavedTenants -SettingsPath $settingsPath $refreshed = 0 $failed = 0 foreach($t in $tenantsToRefresh) { Write-Host " Resolving $($t.TenantId) ..." -ForegroundColor DarkGray -NoNewline $name = Resolve-TenantName -TenantId $t.TenantId -SettingsPath $settingsPath if($name) { Update-TenantNameCache -SettingsPath $settingsPath -TenantId $t.TenantId -TenantName $name Write-Host " -> $name" -ForegroundColor Green $refreshed++ if($t.TenantId -eq $TenantId) { $currentTenant = [PSCustomObject]@{ TenantId = $TenantId; TenantName = $name; Display = "$name ($TenantId)" } } } else { Write-Host " -> FAILED" -ForegroundColor Red $failed++ } } Write-Host "`nRefresh complete. Success: $refreshed, Failed: $failed" -ForegroundColor Cyan Write-Host "`nPress any key to return to the menu..." -ForegroundColor DarkGray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") continue } $scriptPath = Join-Path $projectRoot $script if(-not (Test-Path $scriptPath)) { throw "Script not found: $scriptPath" } Write-Host "`nLaunching $script ...`n" -ForegroundColor Green # Clone params and sanitize for scripts that don't accept the full auth set $launchParams = $commonParams.Clone() if($script -eq "Scripts/Initialize-IntuneAuth.ps1") { @("AppId","Secret","Certificate","AuthMode","RedirectUri","Interactive","Mode","WhatIf") | ForEach-Object { $launchParams.Remove($_) } } if($choiceNumber -eq 14) { $launchParams.Delete = $true } if($choiceNumber -eq 15) { $launchParams.DeleteApp = $true } # Execute in same process so TUI flows naturally & $scriptPath @launchParams if($choiceNumber -eq 14 -or $choiceNumber -eq 15) { Write-Host "`nTenant auth deleted. Exiting." -ForegroundColor Yellow exit 0 } Write-Host "`nPress any key to return to the menu..." -ForegroundColor DarkGray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") }