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
31 changes: 21 additions & 10 deletions Private/Get-CustomApp.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ function Get-CustomApp {
# --- 1. CLOUD SOURCE ---
# We use the "Raw" GitHub URL so we get just the JSON text.
# Structure: https://raw.githubusercontent.com/<User>/<Repo>/<Branch>/<PathToFile>
$CloudUrl = "https://raw.githubusercontent.com/Techary/TecharyGet/BETA/TecharyGet/Private/CustomApps.json"

# raw.githubusercontent.com is CDN-backed and is NOT subject to the
# api.github.com rate limit, so this stays cheap at fleet scale.
$CloudUrl = "https://raw.githubusercontent.com/Techary/TecharyGet/BETA/Private/CustomApps.json"

# --- 2. LOCAL CACHE ---
# We cache the file locally so the script works even if GitHub is briefly down
# or if the machine is offline (using the last known good copy).
$CacheDir = "$env:PROGRAMDATA\TecharyGet"
$CachePath = "$CacheDir\CustomApps_Cache.json"

# --- 3. SYNC LOGIC ---
try {
if (-not (Test-Path $CacheDir)) { New-Item -ItemType Directory -Path $CacheDir -Force | Out-Null }

# Logic: Only download if the cache doesn't exist OR it's older than 60 minutes.
# This prevents spamming GitHub every time you run a command.
$NeedUpdate = $true
Expand All @@ -28,11 +30,20 @@ function Get-CustomApp {

if ($NeedUpdate) {
Write-PackagerLog -Message "Syncing Custom Catalog from GitHub..." -Severity Info
Invoke-WebRequest -Uri $CloudUrl -OutFile $CachePath -UseBasicParsing -ErrorAction Stop

# Download to a staging file and prove it parses before promoting it.
# A captive portal or proxy error page returns HTTP 200 with HTML,
# which would otherwise poison the cache for the next 60 minutes.
$StagePath = "$CachePath.tmp"
Invoke-WebRequest -Uri $CloudUrl -OutFile $StagePath -UseBasicParsing -ErrorAction Stop

$null = (Get-Content -Path $StagePath -Raw | ConvertFrom-Json)
Move-Item -Path $StagePath -Destination $CachePath -Force -ErrorAction Stop
}
}
catch {
Write-PackagerLog -Message "Could not sync from GitHub (Offline?). Using local cache." -Severity Warning
Write-PackagerLog -Message "Could not sync Custom Catalog ($($_.Exception.Message)). Using local copy." -Severity Warning
Remove-Item "$CachePath.tmp" -Force -ErrorAction SilentlyContinue
}

# --- 4. READ DATA ---
Expand All @@ -45,8 +56,8 @@ function Get-CustomApp {
# Fallback to the file shipped with the module (if cache is empty/broken)
else {
$LocalModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Private\CustomApps.json"
if (Test-Path $LocalModulePath) {
$JsonContent = Get-Content -Path $LocalModulePath -Raw
if (Test-Path $LocalModulePath) {
$JsonContent = Get-Content -Path $LocalModulePath -Raw
}
}

Expand All @@ -62,6 +73,6 @@ function Get-CustomApp {
return $null
}
}

return $null
}
}
114 changes: 114 additions & 0 deletions Private/Resolve-GitHubManifest.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
function Get-ManifestVersionKey {
param([string]$Name)

# winget version folders are not reliably [Version]-parseable: "1.2.3-beta",
# "20240101", "2.0" and "1.2.3.4.5" all occur. Casting to [Version] and
# discarding the failures silently dropped real releases, and threw outright
# for packages where no folder happened to parse. Build a zero-padded key so
# ordinary string sorting gives correct numeric ordering instead.
$Numbers = [regex]::Matches($Name, '\d+') | ForEach-Object { $_.Value }
if (-not $Numbers) { return $null }

# Always emit the same number of components. With variable-length keys
# "1.2" sorted ABOVE "1.2.3", because the separator that terminated the
# shorter key compared higher than the '.' in the longer one.
$Parts = New-Object System.Collections.Generic.List[string]
foreach ($N in ($Numbers | Select-Object -First 6)) {
try { $Parts.Add('{0:D12}' -f [int64]$N) } catch { $Parts.Add('{0:D12}' -f 0) }
}
while ($Parts.Count -lt 6) { $Parts.Add('{0:D12}' -f 0) }

# Tiebreak on equal numbers: prefer the stable release over a prerelease,
# so "1.2.3" beats "1.2.3-beta" instead of the winner being arbitrary.
$Rank = if ($Name -match '[A-Za-z]') { '0' } else { '9' }

return (($Parts -join '.') + '|' + $Rank)
}

function Resolve-GitHubManifest {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)][string]$Id,
[Parameter(Mandatory=$true)][string]$SysArch,
[Parameter(Mandatory=$true)][hashtable]$Headers
)

# 2. Construct API Path
$IdPath = $Id.Replace(".", "/")
$FirstChar = $Id.Substring(0,1).ToLower()
$BaseApi = "https://api.github.com/repos/microsoft/winget-pkgs/contents/manifests/$FirstChar/$IdPath"

# 3. Get Version (Latest) [API call 1 of 2]
$VersionsResponse = Invoke-RestMethod -Uri $BaseApi -Method Get -Headers $Headers -ErrorAction Stop

$LatestVersionObj = $VersionsResponse |
Where-Object { $_.type -eq "dir" } |
Select-Object *, @{N='SortKey'; E={ Get-ManifestVersionKey -Name $_.name }} |
Where-Object { $null -ne $_.SortKey } |
Sort-Object SortKey -Descending |
Select-Object -First 1

if (-not $LatestVersionObj) { throw "Could not determine a valid version folder for '$Id'." }
$LatestVersion = $LatestVersionObj.Name

# 4. Get Manifest [API call 2 of 2]
$VersionPath = "$BaseApi/$LatestVersion"
$VersionFiles = Invoke-RestMethod -Uri $VersionPath -Method Get -Headers $Headers -ErrorAction Stop
$InstallerFile = $VersionFiles | Where-Object { $_.name -like "*.installer.yaml" } | Select-Object -First 1
if (-not $InstallerFile) { throw "No installer YAML found for '$Id' $LatestVersion." }

# Served from raw.githubusercontent.com, which is CDN-backed and not
# subject to the API rate limit, so no credentials are sent here.
$YamlContent = Invoke-RestMethod -Uri $InstallerFile.download_url -Headers @{ 'User-Agent' = 'TecharyGet' } -ErrorAction Stop

# --- PARSING LOGIC ---
# We split by "- Architecture" to separate blocks, but keep the delimiter to help identification
$Blocks = $YamlContent -split '(?=-\s*Architecture:)'

$SelectedUrl = $null
$SelectedArgs = $null
$SelectedType = "exe"
$SelectedCode = $null

foreach ($Block in $Blocks) {
if ([string]::IsNullOrWhiteSpace($Block)) { continue }

if ($Block -match 'Architecture:\s*([a-zA-Z0-9]+)') {
$BlockArch = $Matches[1].Trim()

if ($BlockArch -eq $SysArch) {
if ($Block -match 'InstallerUrl:\s*["'']?([^"''\r\n]+)["'']?') { $SelectedUrl = $Matches[1].Trim() }
if ($Block -match 'InstallerType:\s*([a-zA-Z0-9]+)') { $SelectedType = $Matches[1].Trim() }

if ($Block -match 'Silent:\s*(.+)') { $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') }
elseif ($Block -match 'SilentWithProgress:\s*(.+)') { $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') }

if ($Block -match 'ProductCode:\s*["'']?([^"''\r\n]+)["'']?') { $SelectedCode = $Matches[1].Trim() }

if ($SelectedUrl) { break }
}
}
}

# Fallbacks (Global properties if not in block)
if (-not $SelectedUrl) { if ($YamlContent -match 'InstallerUrl:\s*["'']?([^"''\r\n]+)["'']?') { $SelectedUrl = $Matches[1].Trim() } }
if (-not $SelectedArgs) {
if ($YamlContent -match 'Silent:\s*(.+)') { $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') }
}
if (-not $SelectedCode) {
if ($YamlContent -match 'ProductCode:\s*["'']?([^"''\r\n]+)["'']?') { $SelectedCode = $Matches[1].Trim() }
}

if (-not $SelectedUrl) { throw "No InstallerUrl found in the manifest for '$Id' $LatestVersion." }

return [PSCustomObject]@{
Id = $Id
Version = $LatestVersion
Arch = $SysArch
Url = $SelectedUrl
SilentArgs = $SelectedArgs
InstallerType = $SelectedType
ProductCode = $SelectedCode
ResolvedUtc = (Get-Date).ToUniversalTime().ToString('o')
}
}
Loading