bin/octa.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Octa CLI entry point. See specs/001-opt-in-debloat-categories/contracts/cli-contract.md. .EXAMPLE octa --list octa --telemetry octa --telemetry --appx --clean octa --quick --clean --json octa --undo latest #> Import-Module (Join-Path $PSScriptRoot '..\Octa.psd1') -Force # -SkipRiskScan: this only needs Id to validate flags against known categories - it ran on # every single CLI invocation regardless of which flag was passed, paying the full live-scan # cost (~20s across all 20 categories) just to check a flag name. See Get-OctaCategory.ps1. $knownCategoryIds = @(Get-OctaCategory -SkipRiskScan | Select-Object -ExpandProperty Id) $apply = $false $quick = $false $noRestorePoint = $false $yes = $false $asJson = $false $quiet = $false $list = $false $undoTarget = $null $categoryIds = @() $dashboard = $false $startupManager = $false $startupDisable = $null $tasksBrowser = $false $taskEnable = $null $taskDisable = $null $taskRun = $null $taskStop = $null $taskDelete = $null $disableTarget = $null $diskCleanup = $false $secureDelete = $false $uninstaller = $false $removeTarget = $null $cleanLeftovers = $false $networkCleanup = $false $flushDns = $false $removeWifiProfile = $null $clearArp = $false $scheduler = $false $schedulerCreate = $false $schedulerCategory = $null $schedulerFrequency = $null $softwareUpdater = $false $history = $false $diskUsage = $false $diskUsagePath = $null $diskUsageTop = $null $diskHealth = $false $malwareScan = $false $malwareScanFull = $false # ponytail: every flag that takes a value used to do a bare `$i++; $x = $args[$i]`, which reads # past the end of $args as $null when the flag is last (`octa --undo`) - the flag then silently # did nothing and execution fell through to the interactive menu, with no hint that the command # was incomplete. It also happily swallowed the NEXT flag as the value (`octa --undo --json`). function Get-OctaFlagValue { param( [Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Arguments, [Parameter(Mandatory)][int]$ValueIndex, [Parameter(Mandatory)][string]$FlagName ) if ($ValueIndex -ge $Arguments.Count) { Write-Error "Missing value for $FlagName" exit 5 } $value = [string]$Arguments[$ValueIndex] if ($value -like '--*') { Write-Error "Missing value for $FlagName (got the flag '$value' instead)" exit 5 } return $value } $i = 0 while ($i -lt $args.Count) { $arg = $args[$i] # NOTE: PowerShell's switch evaluates every matching case, not just the first (no C-style # implicit fallthrough prevention) - each case needs an explicit `break`, otherwise a flag # like --list also falls into the generic --<category> case below and errors out. switch -Regex ($arg) { '^--clean$' { $apply = $true; break } '^--quick$' { $quick = $true; break } '^--no-restore-point$' { $noRestorePoint = $true; break } '^--yes$' { $yes = $true; break } '^--json$' { $asJson = $true; break } '^(--quiet|-q)$' { $quiet = $true; break } '^--verbose$' { break } # activity panel is verbose by nature - no separate flag behavior needed yet '^--list$' { $list = $true; break } '^--undo$' { $i++; $undoTarget = Get-OctaFlagValue -Arguments $args -ValueIndex $i -FlagName '--undo'; break } '^--dashboard$' { $dashboard = $true; break } '^--startup-manager$' { $startupManager = $true; break } '^--disk-cleanup$' { $diskCleanup = $true; break } '^--secure-delete$' { $secureDelete = $true; break } '^--tasks$' { $tasksBrowser = $true; break } '^--uninstaller$' { $uninstaller = $true; break } '^--remove$' { $i++; $removeTarget = Get-OctaFlagValue -Arguments $args -ValueIndex $i -FlagName '--remove'; break } '^--clean-leftovers$' { $cleanLeftovers = $true; break } '^--network-cleanup$' { $networkCleanup = $true; break } '^--flush-dns$' { $flushDns = $true; break } '^--remove-wifi-profile$' { $i++; $removeWifiProfile = Get-OctaFlagValue -Arguments $args -ValueIndex $i -FlagName '--remove-wifi-profile'; break } '^--clear-arp$' { $clearArp = $true; break } '^--schedule$' { $scheduler = $true; break } '^--create$' { $schedulerCreate = $true; break } '^--category$' { $i++; $schedulerCategory = Get-OctaFlagValue -Arguments $args -ValueIndex $i -FlagName '--category'; break } '^--frequency$' { $i++; $schedulerFrequency = Get-OctaFlagValue -Arguments $args -ValueIndex $i -FlagName '--frequency'; break } '^--update$' { $softwareUpdater = $true; break } '^--history$' { $history = $true; break } '^--disk-usage$' { $diskUsage = $true; break } '^--path$' { $i++; $diskUsagePath = Get-OctaFlagValue -Arguments $args -ValueIndex $i -FlagName '--path'; break } '^--top$' { $i++; $diskUsageTop = Get-OctaFlagValue -Arguments $args -ValueIndex $i -FlagName '--top'; break } '^--disk-health$' { $diskHealth = $true; break } '^--malware-scan$' { $malwareScan = $true; break } '^--full$' { $malwareScanFull = $true; break } '^--disable$' { $i++ # ponytail: --disable is shared by --tasks and --startup-manager. It used to pick the # owner by reading $tasksBrowser mid-parse, so `octa --disable X --tasks` was silently # handled as a startup-manager call because --tasks hadn't been seen yet. Stash the # value and resolve the owner once the whole command line has been read. $disableTarget = Get-OctaFlagValue -Arguments $args -ValueIndex $i -FlagName '--disable' break } '^--enable$' { $i++; $taskEnable = Get-OctaFlagValue -Arguments $args -ValueIndex $i -FlagName '--enable'; break } '^--run$' { $i++; $taskRun = Get-OctaFlagValue -Arguments $args -ValueIndex $i -FlagName '--run'; break } '^--stop$' { $i++; $taskStop = Get-OctaFlagValue -Arguments $args -ValueIndex $i -FlagName '--stop'; break } '^--delete$' { $i++; $taskDelete = Get-OctaFlagValue -Arguments $args -ValueIndex $i -FlagName '--delete'; break } '^--(.+)$' { $candidate = $Matches[1] if ($candidate -in $knownCategoryIds) { $categoryIds += $candidate } else { Write-Error "Unknown flag or category: --$candidate" exit 5 } break } # ponytail: anything that isn't a --flag at all (a bare word, a typo like `octa telemetry` # missing its dashes, a stray path) matched no case and was dropped in silence, so the # command looked like it worked while ignoring what was actually asked for. default { Write-Error "Unexpected argument: $arg (categories and options must start with --)" exit 5 } } $i++ } # Resolved after parsing so --disable works regardless of where --tasks appears on the line. if ($disableTarget) { if ($tasksBrowser) { $taskDisable = $disableTarget } else { $startupDisable = $disableTarget } } if ($list) { Get-OctaCategory | Select-Object Id, DisplayName, RequiresElevation, ContainsIrreversibleActions, HighestRiskLevel | Format-Table -AutoSize exit 0 } if ($undoTarget) { $result = Undo-OctaRun -RunId $undoTarget if ($asJson) { $result | ConvertTo-Json -Depth 6 } else { $result | Format-List } if ($result.Status -eq 'NotFound') { exit 4 } # An undo where nothing could be restored must not report success to a caller/script. if ($result.Status -in @('Failed', 'PartiallyUndone')) { exit 5 } exit 0 } if ($dashboard) { Show-OctaDashboard | Out-Null exit 0 } if ($startupManager) { if ($startupDisable) { $result = Invoke-OctaStartupManager -Disable $startupDisable if ($asJson) { $result | ConvertTo-Json -Depth 6 } else { $result | Format-List } } else { Invoke-OctaStartupManager | Out-Null } exit 0 } if ($tasksBrowser) { if ($taskEnable) { $result = Invoke-OctaTaskBrowser -Enable $taskEnable } elseif ($taskDisable) { $result = Invoke-OctaTaskBrowser -Disable $taskDisable } elseif ($taskRun) { $result = Invoke-OctaTaskBrowser -Run $taskRun } elseif ($taskStop) { $result = Invoke-OctaTaskBrowser -Stop $taskStop } elseif ($taskDelete) { $result = Invoke-OctaTaskBrowser -Delete $taskDelete -Yes:$yes } else { Invoke-OctaTaskBrowser | Out-Null; exit 0 } if ($asJson) { $result | ConvertTo-Json -Depth 6 } else { $result | Format-List } exit 0 } if ($diskCleanup) { $result = Invoke-OctaDiskCleanup -Apply:$apply -Yes:$yes -SecureDelete:$secureDelete if ($asJson) { $result | ConvertTo-Json -Depth 6 } else { $result | Format-List } exit 0 } if ($uninstaller) { if ($removeTarget) { $result = Invoke-OctaUninstaller -Remove $removeTarget } elseif ($cleanLeftovers) { $result = Invoke-OctaUninstaller -CleanLeftovers -Yes:$yes } else { Invoke-OctaUninstaller | Out-Null; exit 0 } if ($asJson) { $result | ConvertTo-Json -Depth 6 } else { $result | Format-List } exit 0 } if ($networkCleanup) { if ($flushDns) { $result = Invoke-OctaNetworkCleanup -FlushDns } elseif ($removeWifiProfile) { $result = Invoke-OctaNetworkCleanup -RemoveWifiProfile $removeWifiProfile } elseif ($clearArp) { $result = Invoke-OctaNetworkCleanup -ClearArp } else { Invoke-OctaNetworkCleanup | Out-Null; exit 0 } if ($asJson) { $result | ConvertTo-Json -Depth 6 } else { $result | Format-List } exit 0 } if ($scheduler) { if ($schedulerCreate) { $result = Invoke-OctaScheduler -Create -Category $schedulerCategory -Frequency $schedulerFrequency } elseif ($removeTarget) { $result = Invoke-OctaScheduler -Remove $removeTarget } else { Invoke-OctaScheduler | Out-Null; exit 0 } if ($asJson) { $result | ConvertTo-Json -Depth 6 } else { $result | Format-List } exit 0 } if ($softwareUpdater) { $result = Invoke-OctaSoftwareUpdater -Apply:$apply -Yes:$yes if ($asJson) { $result | ConvertTo-Json -Depth 6 } else { $result | Format-List } exit 0 } if ($history) { $result = Show-OctaHistory if ($asJson) { $result | ConvertTo-Json -Depth 6 } exit 0 } if ($diskUsage) { $params = @{} if ($diskUsagePath) { $params.Path = $diskUsagePath } if ($diskUsageTop) { $params.Top = [int]$diskUsageTop } $result = Show-OctaDiskUsage @params if ($asJson) { $result | ConvertTo-Json -Depth 6 } exit 0 } if ($diskHealth) { $result = Show-OctaDiskHealth if ($asJson) { $result | ConvertTo-Json -Depth 6 } exit 0 } if ($malwareScan) { $result = Invoke-OctaMalwareScan -Full:$malwareScanFull if ($asJson) { $result | ConvertTo-Json -Depth 6 } else { $result | Format-List } exit 0 } if (-not $quick -and $categoryIds.Count -eq 0) { Invoke-Octa exit 0 } if ($quick) { $result = Invoke-OctaQuickClean -Apply:$apply -NoRestorePoint:$noRestorePoint -Yes:$yes -Quiet:$quiet } else { $result = Invoke-OctaCategory -CategoryId $categoryIds -Apply:$apply -NoRestorePoint:$noRestorePoint -Yes:$yes -Quiet:$quiet } if ($asJson) { $result | ConvertTo-Json -Depth 6 } else { $result | Format-List } switch ($result.Status) { 'Cancelled' { exit 1 } 'ElevationRequired' { exit 3 } 'UnknownCategory' { exit 5 } 'Error' { exit 5 } default { exit 0 } } |