Skip to content
Merged
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
7 changes: 7 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ channels derive from it:
`JacobOptimiza/scoop-bucket` repository (`packaging/scoop/`).
- **WinGet** — immutable versioned manifests with SHA-256
(`packaging/winget/`), submitted to `microsoft/winget-pkgs`.
- **Chocolatey (not published)** — the accepted future design is a
machine-owned package layout with a separate
`.devnav-managed-by-chocolatey` marker and a machine `PSModulePath` entry.
It is not implemented as a distribution channel yet. Chocolatey's Community
verifier runs on Windows Server 2019; DevNav currently supports Windows 10
and 11, so verifier compatibility or an exemption remains an open release
decision.

## `dev` invocation flow

Expand Down
57 changes: 45 additions & 12 deletions powershell/DevNav.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -225,16 +225,25 @@ function Initialize-DevUpdateCheckPreference {
return $enabled
}

function Get-DevInstallationOwner {
# Package managers own the files they install; DevNav must not self-update
# underneath them. Each manager has its own marker next to this module.
$owners = @(
if (Test-Path -LiteralPath (Join-Path $PSScriptRoot '.devnav-managed-by-scoop') -PathType Leaf) { 'scoop' }
if (Test-Path -LiteralPath (Join-Path $PSScriptRoot '.devnav-managed-by-chocolatey') -PathType Leaf) { 'chocolatey' }
)
if ($owners.Count -gt 1) { return 'ambiguous' }
if ($owners.Count -eq 1) { return $owners[0] }
return 'unmanaged'
}

function Test-DevManagedInstallation {
# Package managers such as Scoop own the files they install; DevNav must not
# self-update underneath them. Managed layouts carry a marker file next to
# this module.
return (Test-Path -LiteralPath (Join-Path $PSScriptRoot '.devnav-managed-by-scoop') -PathType Leaf)
return (Get-DevInstallationOwner) -ne 'unmanaged'
}

function Invoke-DevStartupUpdateCheck {
if ([Console]::IsInputRedirected) { return }
if (Test-DevManagedInstallation) { return }
if ((Get-DevInstallationOwner) -ne 'unmanaged') { return }
Initialize-DevLanguage | Out-Null
if (-not (Initialize-DevUpdateCheckPreference)) { return }

Expand Down Expand Up @@ -380,13 +389,37 @@ function Update-DevNavigator {
[CmdletBinding(SupportsShouldProcess)]
param()

if (Test-DevManagedInstallation) {
if ((Get-DevLanguage) -eq 'en-US') {
Write-Host 'This installation is managed by Scoop; DevNav will not self-update.' -ForegroundColor Yellow
Write-Host 'Update it with: scoop update devnav' -ForegroundColor Cyan
} else {
Write-Host 'Esta instalación la gestiona Scoop; DevNav no se actualiza a sí mismo.' -ForegroundColor Yellow
Write-Host 'Para actualizar, ejecuta: scoop update devnav' -ForegroundColor Cyan
$owner = Get-DevInstallationOwner
if ($owner -ne 'unmanaged') {
$english = (Get-DevLanguage) -eq 'en-US'
switch ($owner) {
'scoop' {
if ($english) {
Write-Host 'This installation is managed by Scoop; DevNav will not self-update.' -ForegroundColor Yellow
Write-Host 'Update it with: scoop update devnav' -ForegroundColor Cyan
} else {
Write-Host 'Esta instalación la gestiona Scoop; DevNav no se actualiza a sí mismo.' -ForegroundColor Yellow
Write-Host 'Para actualizar, ejecuta: scoop update devnav' -ForegroundColor Cyan
}
}
'chocolatey' {
if ($english) {
Write-Host 'This installation is managed by Chocolatey; DevNav will not self-update.' -ForegroundColor Yellow
Write-Host 'Update it with: choco upgrade devnav' -ForegroundColor Cyan
} else {
Write-Host 'Esta instalación la gestiona Chocolatey; DevNav no se actualiza a sí mismo.' -ForegroundColor Yellow
Write-Host 'Para actualizar, ejecuta: choco upgrade devnav' -ForegroundColor Cyan
}
}
'ambiguous' {
if ($english) {
Write-Host 'DevNav ownership is ambiguous: Scoop and Chocolatey markers are both present. DevNav will not self-update.' -ForegroundColor Red
Write-Host 'Remove the incompatible marker before choosing a package manager.' -ForegroundColor Yellow
} else {
Write-Host 'La propiedad de DevNav es ambigua: existen markers de Scoop y Chocolatey. DevNav no se actualiza a sí mismo.' -ForegroundColor Red
Write-Host 'Elimina el marker incompatible antes de elegir un gestor de paquetes.' -ForegroundColor Yellow
}
}
}
return
}
Expand Down
96 changes: 96 additions & 0 deletions tests/powershell/DevNav.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,102 @@ Describe 'DevNav Scoop-managed installation' {
}
}

Describe 'DevNav package-manager ownership' {
BeforeAll {
$chocolateyModuleRoot = Join-Path $testLocalAppData 'chocolatey-module'
$ambiguousModuleRoot = Join-Path $testLocalAppData 'ambiguous-module'
foreach ($root in @($chocolateyModuleRoot, $ambiguousModuleRoot)) {
New-Item -ItemType Directory -Path $root -Force | Out-Null
Copy-Item -LiteralPath $modulePath -Destination (Join-Path $root 'DevNav.psm1')
}
New-Item -ItemType File -Path (Join-Path $chocolateyModuleRoot '.devnav-managed-by-chocolatey') -Force | Out-Null
New-Item -ItemType File -Path (Join-Path $ambiguousModuleRoot '.devnav-managed-by-scoop') -Force | Out-Null
New-Item -ItemType File -Path (Join-Path $ambiguousModuleRoot '.devnav-managed-by-chocolatey') -Force | Out-Null
Import-Module (Join-Path $chocolateyModuleRoot 'DevNav.psm1') -Force
Import-Module (Join-Path $ambiguousModuleRoot 'DevNav.psm1') -Force
$chocolateyModule = Get-Module | Where-Object { $_.Path -eq (Join-Path $chocolateyModuleRoot 'DevNav.psm1') } | Select-Object -First 1
$ambiguousModule = Get-Module | Where-Object { $_.Path -eq (Join-Path $ambiguousModuleRoot 'DevNav.psm1') } | Select-Object -First 1
}

It 'keeps unmanaged installations available for normal self-update' {
$devModule.Invoke({ Get-DevInstallationOwner }) | Should -Be 'unmanaged'
$devModule.Invoke({ Test-DevManagedInstallation }) | Should -BeFalse
}

It 'detects Chocolatey ownership without requiring Chocolatey on the test machine' {
$chocolateyModule.Invoke({ Get-DevInstallationOwner }) | Should -Be 'chocolatey'
$chocolateyModule.Invoke({ Test-DevManagedInstallation }) | Should -BeTrue
}

It 'detects ambiguous ownership when both markers exist' {
$ambiguousModule.Invoke({ Get-DevInstallationOwner }) | Should -Be 'ambiguous'
$ambiguousModule.Invoke({ Test-DevManagedInstallation }) | Should -BeTrue
}

It 'skips the startup update check for Chocolatey ownership' {
$chocolateyModule.Invoke({
Mock Initialize-DevUpdateCheckPreference { throw 'Chocolatey startup must not initialize update preferences.' }
Mock Get-DevLatestRelease { throw 'Chocolatey startup must not query GitHub.' }
{ Invoke-DevStartupUpdateCheck } | Should -Not -Throw
})
}

It 'skips the startup update check for ambiguous ownership' {
$ambiguousModule.Invoke({
Mock Initialize-DevUpdateCheckPreference { throw 'Ambiguous startup must not initialize update preferences.' }
Mock Get-DevLatestRelease { throw 'Ambiguous startup must not query GitHub.' }
{ Invoke-DevStartupUpdateCheck } | Should -Not -Throw
})
}

It 'reports Chocolatey instructions in English without downloading' {
$global:DevNavChocolateyMessages = [System.Collections.Generic.List[string]]::new()
$global:DevNavChocolateyLatestCalls = 0
$global:DevNavChocolateyDownloadCalls = 0
$chocolateyModule.Invoke({
Mock Get-DevLanguage { 'en-US' }
Mock Get-DevLatestRelease { $global:DevNavChocolateyLatestCalls++ }
Mock Invoke-DevDownload { $global:DevNavChocolateyDownloadCalls++ }
Mock Write-Host { [void]$global:DevNavChocolateyMessages.Add([string]$Object) }
Update-DevNavigator -Confirm:$false
})
$global:DevNavChocolateyMessages | Should -Contain 'This installation is managed by Chocolatey; DevNav will not self-update.'
$global:DevNavChocolateyMessages | Should -Contain 'Update it with: choco upgrade devnav'
$global:DevNavChocolateyLatestCalls | Should -Be 0
$global:DevNavChocolateyDownloadCalls | Should -Be 0
Remove-Variable DevNavChocolateyMessages, DevNavChocolateyLatestCalls, DevNavChocolateyDownloadCalls -Scope Global -ErrorAction SilentlyContinue
}

It 'reports Chocolatey instructions in Spanish without downloading' {
$global:DevNavChocolateyMessages = [System.Collections.Generic.List[string]]::new()
$chocolateyModule.Invoke({
Mock Get-DevLanguage { 'es-ES' }
Mock Write-Host { [void]$global:DevNavChocolateyMessages.Add([string]$Object) }
Update-DevNavigator -Confirm:$false
})
$global:DevNavChocolateyMessages | Should -Contain 'Esta instalación la gestiona Chocolatey; DevNav no se actualiza a sí mismo.'
$global:DevNavChocolateyMessages | Should -Contain 'Para actualizar, ejecuta: choco upgrade devnav'
Remove-Variable DevNavChocolateyMessages -Scope Global -ErrorAction SilentlyContinue
}

It 'fails safe for ambiguous ownership without downloading' {
$global:DevNavAmbiguousMessages = [System.Collections.Generic.List[string]]::new()
$global:DevNavAmbiguousLatestCalls = 0
$global:DevNavAmbiguousDownloadCalls = 0
$ambiguousModule.Invoke({
Mock Get-DevLanguage { 'en-US' }
Mock Get-DevLatestRelease { $global:DevNavAmbiguousLatestCalls++ }
Mock Invoke-DevDownload { $global:DevNavAmbiguousDownloadCalls++ }
Mock Write-Host { [void]$global:DevNavAmbiguousMessages.Add([string]$Object) }
Update-DevNavigator -Confirm:$false
})
($global:DevNavAmbiguousMessages -join "`n") | Should -Match 'ownership is ambiguous'
$global:DevNavAmbiguousLatestCalls | Should -Be 0
$global:DevNavAmbiguousDownloadCalls | Should -Be 0
Remove-Variable DevNavAmbiguousMessages, DevNavAmbiguousLatestCalls, DevNavAmbiguousDownloadCalls -Scope Global -ErrorAction SilentlyContinue
}
}

Describe 'DevNav shortcut commands' {
BeforeAll {
cargo build --quiet --manifest-path (Join-Path $repositoryRoot 'Cargo.toml')
Expand Down