feat(toolkit): complete macOS Intune Toolkit v1
Core enhancements: - Expanded default export/import scope to ~45 object types including DeviceManagementIntents - Added -AllPages pagination support across Graph queries for large tenants - Invoke-GraphRequest now throws on 4xx/5xx instead of silently returning null - Added macOS Keychain fallback for secret retrieval in headless auth flow - Added NameSearchPattern/NameReplacePattern mutation support through export/import forms New toolkit scripts: - Bulk-AppAssignment.ps1: bulk-assign apps to groups/All Users/All Devices - Bulk-AssignmentManager.ps1: add/remove assignments for any policy type with correct @odata.type - Backup-Restore-Assignments.ps1: JSON backup with cross-tenant group resolution - Export-AssignmentsToCsv.ps1: CSV/Markdown documentation output - Bulk-RenamePolicies.ps1: regex search/replace and prefix mutations - Bulk-DeviceOperations.ps1: delete/retire/wipe/lock/sync with -WhatIf safeguards - Start-IntuneManagementTui.ps1: interactive terminal UI for headless operations - Create-IntuneManagementApp.ps1: helper for app registration setup Updated existing scripts: - Export-Policies.ps1 / Import-Policies.ps1: wired mutation params through - Start-HeadlessIntune.ps1: integrated TUI and new parameter forwarding
This commit is contained in:
245
Scripts/Start-IntuneManagementTui.ps1
Normal file
245
Scripts/Start-IntuneManagementTui.ps1
Normal file
@@ -0,0 +1,245 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Interactive terminal UI for IntuneManagement headless export/import.
|
||||
.DESCRIPTION
|
||||
Prompts for action, tenant, paths, filters, object types, and toggles.
|
||||
Returns a PSCustomObject that Start-HeadlessIntune.ps1 consumes.
|
||||
Uses fzf on macOS/Linux when available; falls back to numbered menus.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
#region Helper functions
|
||||
function Test-FzfAvailable
|
||||
{
|
||||
return [bool](Get-Command fzf -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
function Show-FzfMenu
|
||||
{
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string[]]$Items,
|
||||
[string]$Header = "Select one",
|
||||
[switch]$Multi
|
||||
)
|
||||
$argsList = @("--header=$Header")
|
||||
if($Multi) { $argsList += "--multi" }
|
||||
$selected = $Items | fzf @argsList --bind=space:toggle
|
||||
if(-not $selected) { return $null }
|
||||
if($Multi) { return @($selected -split "`r?`n" | Where-Object { $_ }) }
|
||||
return $selected
|
||||
}
|
||||
|
||||
function Show-NumberedMenu
|
||||
{
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string[]]$Items,
|
||||
[string]$Header = "Select one or more",
|
||||
[switch]$Multi
|
||||
)
|
||||
Write-Host "`n$Header" -ForegroundColor Cyan
|
||||
for($i=0; $i -lt $Items.Count; $i++)
|
||||
{
|
||||
Write-Host " $($i+1). $($Items[$i])"
|
||||
}
|
||||
if($Multi)
|
||||
{
|
||||
$prompt = "Enter numbers separated by commas (e.g. 1,3,5) or 'all'"
|
||||
}
|
||||
else
|
||||
{
|
||||
$prompt = "Enter a number"
|
||||
}
|
||||
$choice = Read-Host $prompt
|
||||
if($choice -eq "all" -and $Multi) { return $Items }
|
||||
$indices = $choice -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -match "^\d+$" } | ForEach-Object { [int]$_ - 1 } | Where-Object { $_ -ge 0 -and $_ -lt $Items.Count }
|
||||
if($Multi)
|
||||
{
|
||||
return $Items[$indices] | Select-Object -Unique
|
||||
}
|
||||
else
|
||||
{
|
||||
if($indices.Count -eq 0) { return $null }
|
||||
return $Items[$indices[0]]
|
||||
}
|
||||
}
|
||||
|
||||
function Select-MenuItem
|
||||
{
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string[]]$Items,
|
||||
[string]$Header = "Select one",
|
||||
[switch]$Multi
|
||||
)
|
||||
if(Test-FzfAvailable)
|
||||
{
|
||||
return Show-FzfMenu -Items $Items -Header $Header -Multi:$Multi
|
||||
}
|
||||
return Show-NumberedMenu -Items $Items -Header $Header -Multi:$Multi
|
||||
}
|
||||
|
||||
function Read-YesNo
|
||||
{
|
||||
param(
|
||||
[string]$Prompt,
|
||||
[bool]$Default = $false
|
||||
)
|
||||
$defaultChar = if($Default) { "Y" } else { "N" }
|
||||
$response = Read-Host "$Prompt [Y/n] (default: $defaultChar)"
|
||||
if([string]::IsNullOrWhiteSpace($response)) { return $Default }
|
||||
return $response -match "^\s*y"
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Load defaults
|
||||
$modulePath = Join-Path (Split-Path -Parent $PSScriptRoot) "Headless/IntuneManagement.Headless.psd1"
|
||||
Import-Module $modulePath -Force
|
||||
|
||||
$defaultTypes = Get-DefaultIntunePolicyObjectTypes
|
||||
$settingsPath = Get-DefaultSettingsPath
|
||||
$preloadedTenantId = $null
|
||||
if(Test-Path $settingsPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
$settings = Get-Content $settingsPath -Raw | ConvertFrom-Json
|
||||
if($settings.TenantId) { $preloadedTenantId = $settings.TenantId }
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
#endregion
|
||||
|
||||
while($true)
|
||||
{
|
||||
Clear-Host
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " IntuneManagement Terminal UI" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " Press Esc to go back, Space to select" -ForegroundColor DarkGray
|
||||
|
||||
# 1. Action
|
||||
$action = Select-MenuItem -Items @("Export","Import") -Header "Select action"
|
||||
if(-not $action) { continue }
|
||||
|
||||
# 2. TenantId
|
||||
$tenantPrompt = "Enter Tenant ID"
|
||||
if($preloadedTenantId) { $tenantPrompt += " (default: $preloadedTenantId)" }
|
||||
$tenantId = Read-Host $tenantPrompt
|
||||
if([string]::IsNullOrWhiteSpace($tenantId)) { $tenantId = $preloadedTenantId }
|
||||
if([string]::IsNullOrWhiteSpace($tenantId)) { Write-Host "Tenant ID is required." -ForegroundColor Red; continue }
|
||||
|
||||
# 3. Object Types
|
||||
Write-Host "`nObject type selection..." -ForegroundColor Cyan
|
||||
$typeSelection = Select-MenuItem -Items $defaultTypes -Header "Select object types to include (Space to multi-select)" -Multi
|
||||
if(-not $typeSelection) { continue }
|
||||
|
||||
# 4. Path
|
||||
$pathPrompt = if($action -eq "Export") { "Enter export root folder path" } else { "Enter import root folder path" }
|
||||
$path = Read-Host $pathPrompt
|
||||
if([string]::IsNullOrWhiteSpace($path)) { Write-Host "Path is required." -ForegroundColor Red; return $null }
|
||||
|
||||
# 5. Name Filter
|
||||
$nameFilter = Read-Host "Name filter regex (optional, e.g. '^Win-OIB-')"
|
||||
|
||||
# 6. Name Mutation
|
||||
$nameSearchPattern = Read-Host "Name search regex for mutation (optional, e.g. '^Win-OIB-')"
|
||||
$nameReplacePattern =
|
||||
if(-not [string]::IsNullOrWhiteSpace($nameSearchPattern))
|
||||
{
|
||||
$nameReplacePattern = Read-Host "Replacement string (e.g. 'Win-TEST-')"
|
||||
}
|
||||
|
||||
# 7. Import-specific options
|
||||
$importType = $null
|
||||
$includeScopeTags = $false
|
||||
$replaceDependencyIds = $false
|
||||
if($action -eq "Import")
|
||||
{
|
||||
$importType = Select-MenuItem -Items @("alwaysImport","skipIfExist","replace","replace_with_assignments","update") -Header "Select import behavior"
|
||||
if(-not $importType) { $importType = "alwaysImport" }
|
||||
$includeScopeTags = Read-YesNo -Prompt "Import scope tags?" -Default $false
|
||||
$replaceDependencyIds = Read-YesNo -Prompt "Replace dependency IDs?" -Default $false
|
||||
}
|
||||
|
||||
# 8. Common toggles
|
||||
$includeAssignments = Read-YesNo -Prompt "Include assignments?" -Default $false
|
||||
$addCompanyName = $false
|
||||
if($action -eq "Export")
|
||||
{
|
||||
$addCompanyName = Read-YesNo -Prompt "Add company name to folders?" -Default $false
|
||||
}
|
||||
|
||||
# 9. Review
|
||||
Clear-Host
|
||||
Write-Host "Review your selection:" -ForegroundColor Green
|
||||
Write-Host " Action : $action"
|
||||
Write-Host " TenantId : $tenantId"
|
||||
Write-Host " Object Types : $($typeSelection -join ', ')"
|
||||
if($action -eq "Export")
|
||||
{
|
||||
Write-Host " Export Path : $path"
|
||||
Write-Host " Add Company Name : $addCompanyName"
|
||||
}
|
||||
else
|
||||
{
|
||||
Write-Host " Import Path : $path"
|
||||
Write-Host " Import Type : $importType"
|
||||
Write-Host " Include Scope Tags : $includeScopeTags"
|
||||
Write-Host " Replace Dep IDs : $replaceDependencyIds"
|
||||
}
|
||||
Write-Host " Name Filter : $(if($nameFilter){$nameFilter}else{'(none)'})"
|
||||
Write-Host " Name Search Pattern : $(if($nameSearchPattern){$nameSearchPattern}else{'(none)'})"
|
||||
Write-Host " Name Replace Pattern: $(if($nameReplacePattern){$nameReplacePattern}else{'(none)'})"
|
||||
Write-Host " Include Assignments : $includeAssignments"
|
||||
|
||||
$confirm = Read-Host "`nProceed? [Y/n] (or type 'back' to restart)"
|
||||
if($confirm -eq "back") { continue }
|
||||
if(-not ([string]::IsNullOrWhiteSpace($confirm) -or $confirm -match "^\s*y"))
|
||||
{
|
||||
Write-Host "Cancelled." -ForegroundColor Yellow
|
||||
continue
|
||||
}
|
||||
|
||||
# 10. Build result
|
||||
$result = [PSCustomObject]@{
|
||||
Action = $action
|
||||
TenantId = $tenantId
|
||||
ObjectTypes = $typeSelection
|
||||
NameFilter = $nameFilter
|
||||
NameSearchPattern = $nameSearchPattern
|
||||
NameReplacePattern = $nameReplacePattern
|
||||
IncludeAssignments = $includeAssignments
|
||||
}
|
||||
|
||||
if($action -eq "Export")
|
||||
{
|
||||
$result | Add-Member -NotePropertyName ExportPath -NotePropertyValue $path
|
||||
$result | Add-Member -NotePropertyName AddCompanyName -NotePropertyValue $addCompanyName
|
||||
}
|
||||
else
|
||||
{
|
||||
$result | Add-Member -NotePropertyName ImportPath -NotePropertyValue $path
|
||||
$result | Add-Member -NotePropertyName ImportType -NotePropertyValue $importType
|
||||
$result | Add-Member -NotePropertyName IncludeScopeTags -NotePropertyValue $includeScopeTags
|
||||
$result | Add-Member -NotePropertyName ReplaceDependencyIds -NotePropertyValue $replaceDependencyIds
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
Reference in New Issue
Block a user