Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ This interactive tool allows you to check status, export lists, and install pack
Notes:
- If Chocolatey is missing, the main menu will show an "Install Chocolatey" option.
- The footer shows the installed Chocolatey version and, when different, the latest available version.
- The wrappers now invoke the project scripts directly instead of forcing a blanket process-scope execution policy bypass.

## Project Layout

Expand Down Expand Up @@ -96,10 +97,13 @@ The script will:
## Security Guidance

- Run the scripts from a trusted, access-controlled directory.
- Prefer `RemoteSigned` or stronger execution policies when possible. If you must use process-scope bypass, ensure files are from a trusted source.
- Review `choco_packages.txt` before running install/sync actions.
- Prefer `RemoteSigned` or stronger execution policies. The project no longer forces routine `ExecutionPolicy Bypass` for normal menu and wrapper launches.
- Chocolatey installs, searches, and upgrades are restricted to `https://community.chocolatey.org/api/v2/` by default.
- Winget installs and searches are restricted to the `winget` source by default.
- Review `data/choco_packages.txt` before running install, update, or sync actions. Managed package list reads and writes are restricted to the repository `data\` directory.
- The Chocolatey bootstrap flow now downloads the package locally, shows its SHA256 hash, and requires explicit confirmation before executing the local installer.
- Avoid copying scripts from unknown sources or locations with weak ACLs.
- Treat `choco-manager.log` as sensitive inventory data; restrict access where appropriate.
- Treat `logs/choco-manager.log` as sensitive inventory data. By default, only warning and error events are persisted; set `CHOCO_MANAGER_LOG_FILE_LEVELS` if you need broader local audit logging.

## Disclaimer

Expand Down
2 changes: 1 addition & 1 deletion choco-pack-install.ps1
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Wrapper script for choco-pack-install
powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-pack-install.ps1") @args
& (Join-Path $PSScriptRoot "src\Choco\choco-pack-install.ps1") @args

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wrapper now calls src/Choco/choco-pack-install.ps1 in-process. That script contains several exit statements (e.g., after elevation relaunch/cancel), which will terminate the caller’s entire PowerShell session if this wrapper is run from an interactive prompt. Prefer spawning a separate PowerShell process (without forcing ExecutionPolicy Bypass) or refactoring the target script to use return/exceptions.

Suggested change
& (Join-Path $PSScriptRoot "src\Choco\choco-pack-install.ps1") @args
$scriptPath = Join-Path $PSScriptRoot "src\Choco\choco-pack-install.ps1"
$powerShellExe = (Get-Process -Id $PID).Path
$argumentList = @('-File', $scriptPath) + $args
$process = Start-Process -FilePath $powerShellExe -ArgumentList $argumentList -Wait -PassThru
exit $process.ExitCode

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion choco-package-explorer.ps1
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Wrapper script for choco-package-explorer
powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-package-explorer.ps1") @args
& (Join-Path $PSScriptRoot "src\Choco\choco-package-explorer.ps1") @args
2 changes: 1 addition & 1 deletion choco-sync.ps1
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Wrapper script for choco-sync
powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-sync.ps1") @args
& (Join-Path $PSScriptRoot "src\Choco\choco-sync.ps1") @args

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wrapper now runs src/Choco/choco-sync.ps1 via &, which executes it in the current PowerShell process. The target script uses exit after requesting elevation, so running the wrapper from an existing PowerShell session will unexpectedly close that session. Consider starting a new PowerShell process (no execution policy bypass required) or updating the target script to avoid exit.

Suggested change
& (Join-Path $PSScriptRoot "src\Choco\choco-sync.ps1") @args
$scriptPath = Join-Path $PSScriptRoot "src\Choco\choco-sync.ps1"
$powerShellPath = (Get-Process -Id $PID).Path
$argumentList = @('-NoProfile', '-File', $scriptPath) + $args
$process = Start-Process -FilePath $powerShellPath -ArgumentList $argumentList -Wait -PassThru
[Environment]::ExitCode = $process.ExitCode

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion choco-upgrade-interactive.ps1
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Wrapper script for choco-upgrade-interactive
powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-upgrade-interactive.ps1") @args
& (Join-Path $PSScriptRoot "src\Choco\choco-upgrade-interactive.ps1") @args

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wrapper now executes src/Choco/choco-upgrade-interactive.ps1 in-process. That target script calls exit when it relaunches for elevation, which will terminate the user’s current PowerShell session if they run this wrapper interactively. Recommend launching a separate PowerShell process (without blanket policy bypass) or changing the target script to return instead of exit.

Suggested change
& (Join-Path $PSScriptRoot "src\Choco\choco-upgrade-interactive.ps1") @args
$targetScript = Join-Path $PSScriptRoot "src\Choco\choco-upgrade-interactive.ps1"
$powerShellExe = if ($PSVersionTable.PSEdition -eq 'Core') {
Join-Path $PSHOME 'pwsh.exe'
} else {
Join-Path $PSHOME 'powershell.exe'
}
& $powerShellExe -NoProfile -File $targetScript @args

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion choco-utils.ps1
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Wrapper script for choco-utils
powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-utils.ps1") @args
& (Join-Path $PSScriptRoot "src\Choco\choco-utils.ps1") @args

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wrapper now runs src/Choco/choco-utils.ps1 in the current PowerShell process. That script uses exit after relaunching for elevation, which will close the caller’s session if the wrapper is run from an interactive prompt. Prefer spawning a new PowerShell process (no policy bypass needed) or replacing exit in the target script with return/exceptions.

Suggested change
& (Join-Path $PSScriptRoot "src\Choco\choco-utils.ps1") @args
$targetScript = Join-Path $PSScriptRoot "src\Choco\choco-utils.ps1"
$powerShellExe = if ($PSVersionTable.PSEdition -eq 'Core') { 'pwsh' } else { 'powershell' }
& $powerShellExe -NoProfile -File $targetScript @args

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion list-choco-apps.ps1
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Wrapper script for list-choco-apps
powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\list-choco-apps.ps1") @args
& (Join-Path $PSScriptRoot "src\Choco\list-choco-apps.ps1") @args

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wrapper now invokes the underlying script in-process via & ... @args. The target script (src/Choco/list-choco-apps.ps1) calls exit on failure, which will terminate the caller’s entire PowerShell session when this wrapper is run interactively. Consider launching a separate PowerShell process (without ExecutionPolicy Bypass) or refactoring the target script to use return/exceptions instead of exit.

Suggested change
& (Join-Path $PSScriptRoot "src\Choco\list-choco-apps.ps1") @args
$targetScript = Join-Path $PSScriptRoot "src\Choco\list-choco-apps.ps1"
$powerShellCommand = if (Get-Command pwsh -ErrorAction SilentlyContinue) { "pwsh" } else { "powershell" }
& $powerShellCommand -NoProfile -File $targetScript @args

Copilot uses AI. Check for mistakes.
4 changes: 1 addition & 3 deletions scripts/choco-manager.ps1
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# Entry script for Choco-Manager

# Load core functions with process-scope policy + unblock
# Load core functions
$corePath = Join-Path (Resolve-Path (Join-Path $PSScriptRoot "..")) "src\Core\core-functions.ps1"
if (-not (Test-Path $corePath)) {
Write-Error "Core functions not found at $corePath"
exit 1
}

try {
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
Unblock-File -Path $corePath -ErrorAction SilentlyContinue
. $corePath
} catch {
Write-Error "Failed to load core functions: $($_.Exception.Message)"
Expand Down
49 changes: 29 additions & 20 deletions scripts/main-menu.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ function Show-Footer {
$chocoInfo = Get-ChocoVersionInfo
$chocoVer = if ($chocoInfo.IsInstalled) { $chocoInfo.InstalledVersion } else { $null }
$chocoLatest = $chocoInfo.LatestVersion
$wingetVer = winget --version 2>$null
$wingetVer = $null
try {
$wingetVer = ((Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("--version")) | Select-Object -First 1)
}
catch { }
Write-Host "----------------------------" -ForegroundColor Gray
if ($chocoVer) {
if ($chocoLatest -and $chocoLatest -ne $chocoVer) {
Expand All @@ -41,7 +45,7 @@ function Show-PackageList {
Write-Host "`nFetching local packages..." -ForegroundColor Gray
$items = @()

$chocoRaw = choco list -lo -r
$chocoRaw = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("list", "-lo", "-r")
foreach ($line in $chocoRaw) {
if ($line -match '\|') {
$parts = $line -split '\|'
Expand All @@ -59,9 +63,8 @@ function Show-PackageList {
}
}

$wingetCmd = Get-Command winget -ErrorAction SilentlyContinue
if ($wingetCmd) {
$wingetRaw = winget list
try {
$wingetRaw = Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("list")
if ($LASTEXITCODE -eq 0) {
foreach ($line in $wingetRaw) {
if ($line -match '^\s*Name\s+Id\s+Version') { continue }
Expand Down Expand Up @@ -89,6 +92,9 @@ function Show-PackageList {
Write-Log "Winget list failed (Exit Code: $LASTEXITCODE)." "WARN"
}
}
catch {
Write-Log "Winget list failed: $($_.Exception.Message)" "WARN"
}

if ($items.Count -eq 0) {
Write-Log "No local packages found." "WARN"
Expand All @@ -115,14 +121,14 @@ function Show-PackageList {
$safeId = Get-ValidatedPackageId -Id $item.Id -Context "Winget"
if ($safeId) {
Write-Host "`n--- Winget Info for $safeId ---" -ForegroundColor Yellow
winget show --id $safeId
Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("show", "--id", $safeId, "--exact", "--source", (Get-TrustedWingetSource))
Pause
}
} else {
$safeId = Get-ValidatedPackageId -Id $item.Id -Context "Chocolatey"
if ($safeId) {
Write-Host "`n--- Chocolatey Info for $safeId ---" -ForegroundColor Yellow
choco info $safeId
Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("info", $safeId, "--source", (Get-TrustedChocolateySource))
Pause
}
}
Expand Down Expand Up @@ -181,7 +187,7 @@ do {
"help" { Show-CommandHelp; Pause }
"quit" { return }
"list" { Show-PackageList; Pause }
"search" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") }
"search" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") }
"logs" { Show-AuditLog; Pause }
Default {
if ($cmd) { Write-Host "Unknown command: $cmd" -ForegroundColor Yellow; Pause }
Expand All @@ -192,22 +198,25 @@ do {

switch ($choice) {
"List/View Packages (Combined)" { Show-PackageList; Pause }
"List Local Packages (Choco)" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") -Action ListChoco; Pause }
"List Local Packages (Winget)" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Winget\winget-utils.ps1") -Action ListOnly; Pause }
"Export/Update List from Local" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\list-choco-apps.ps1"); Pause }
"Install Missing Packages" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-pack-install.ps1"); Pause }
"Synchronize (Full Match)" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-sync.ps1") -Action Sync; Pause }
"Interactive Update (Selectable)" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-upgrade-interactive.ps1"); Pause }
"Update All Packages (Silent)" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-utils.ps1") -Action Update; Pause }
"Search Chocolatey Repository" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") }
"Search Winget Repository" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Winget\winget-utils.ps1") -Action Search }
"List Local Packages (Choco)" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") -ArgumentList @("-Action", "ListChoco"); Pause }
"List Local Packages (Winget)" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Winget\winget-utils.ps1") -ArgumentList @("-Action", "ListOnly"); Pause }
"Export/Update List from Local" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\list-choco-apps.ps1"); Pause }
"Install Missing Packages" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-pack-install.ps1"); Pause }
"Synchronize (Full Match)" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-sync.ps1") -ArgumentList @("-Action", "Sync"); Pause }
"Interactive Update (Selectable)" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-upgrade-interactive.ps1"); Pause }
"Update All Packages (Silent)" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-utils.ps1") -ArgumentList @("-Action", "Update"); Pause }
"Search Chocolatey Repository" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") }
"Search Winget Repository" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Winget\winget-utils.ps1") -ArgumentList @("-Action", "Search") }
"Package Info (by name)" {
$pkg = Read-Host "Enter package name"
$safePkg = Get-ValidatedPackageId -Id $pkg -Context "Chocolatey"
if ($safePkg) { choco info $safePkg; Pause }
if ($safePkg) {
Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("info", $safePkg, "--source", (Get-TrustedChocolateySource))
Pause
}
}
"Package Utilities" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") }
"Winget Tools" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Winget\winget-utils.ps1") -Action Interactive }
"Package Utilities" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") }
"Winget Tools" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Winget\winget-utils.ps1") -ArgumentList @("-Action", "Interactive") }
"View Audit Log" { Show-AuditLog; Pause }
"Elevate to Admin" {
Invoke-ElevatedAction -FilePath (Join-Path $PSScriptRoot "choco-manager.ps1")
Expand Down
47 changes: 31 additions & 16 deletions src/Choco/choco-pack-install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ param(
. (Join-Path (Resolve-Path (Join-Path $PSScriptRoot "..\..")) "src\Core\core-functions.ps1")

if (-not $InputFile) {
$scriptRoot = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path }
$InputFile = Join-Path (Resolve-Path (Join-Path $scriptRoot "..\..")) "data\choco_packages.txt"
$InputFile = Get-DefaultPackageListPath
}

$InputFile = Resolve-ManagedDataFilePath -Path $InputFile -Purpose "package list"

if (-not (Test-IsAdmin)) {
Write-Log "Elevation required for installation. Re-launching..." "WARN"
Invoke-ElevatedAction -FilePath $MyInvocation.MyCommand.Path -ArgumentList @("-InputFile", $InputFile)
Expand All @@ -22,35 +23,49 @@ Write-Log "Starting installation process..." "INFO"

if (-not $SkipUpgradeChoco) {
Write-Log "Checking for Chocolatey updates..." "INFO"
choco upgrade chocolatey -y
Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("upgrade", "chocolatey", "-y", "--source", (Get-TrustedChocolateySource))
}

# Get currently installed packages for comparison
$installedPackages = choco list --local-only --limit-output | ForEach-Object { $_.Split('|')[0] }
$installedPackages = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("list", "--local-only", "--limit-output") | ForEach-Object { $_.Split('|')[0] }
$installedPackageSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($installedPackage in $installedPackages) {
if (-not [string]::IsNullOrWhiteSpace($installedPackage)) {
$null = $installedPackageSet.Add($installedPackage)
}
}

# Read target packages using core helper
$targetPackages = Get-PackageList -Path $InputFile
$targetPackages = Get-PackageList -Path $InputFile

if ($targetPackages.Count -eq 0) {
Write-Log "No packages found in $InputFile to install." "WARN"
exit
}

foreach ($packageName in $targetPackages) {
$missingPackages = $targetPackages | Where-Object { -not $installedPackageSet.Contains($_) }
if ($missingPackages.Count -eq 0) {
Write-Log "All packages from $InputFile are already installed." "SUCCESS"
exit
}

Write-Host "Trusted Chocolatey source: $(Get-TrustedChocolateySource)" -ForegroundColor DarkGray
if (-not (Read-Confirmation -Prompt "Install $($missingPackages.Count) missing package(s)? Type y to continue" -ExpectedValue "y")) {
Write-Log "Installation cancelled by user." "WARN"
exit
}

foreach ($packageName in $missingPackages) {
$safeName = Get-ValidatedPackageId -Id $packageName -Context "Chocolatey"
if (-not $safeName) { continue }
if ($installedPackages -contains $safeName) {
Write-Log "Skipping '$safeName' - already installed." "INFO"

Write-Log "Installing '$safeName'..." "INFO"
Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("install", $safeName, "-y", "--source", (Get-TrustedChocolateySource))
if ($LASTEXITCODE -eq 0) {
Write-Log "Successfully installed '$safeName'." "SUCCESS"
}
else {
Write-Log "Installing '$safeName'..." "INFO"
choco install $safeName -y
if ($LASTEXITCODE -eq 0) {
Write-Log "Successfully installed '$safeName'." "SUCCESS"
}
else {
Write-Log "Failed to install '$safeName' (Exit Code: $LASTEXITCODE)." "ERROR"
}
Write-Log "Failed to install '$safeName' (Exit Code: $LASTEXITCODE)." "ERROR"
}
}

Expand Down
Loading