chore: import upstream snapshot with attribution
@@ -0,0 +1,98 @@
|
||||
---
|
||||
name: translation-diff-export
|
||||
description: Compares UniGetUI JSON locale files against English, identifies untranslated or source-changed keys, and generates patch, reference, and handoff files for a target language. Use when the user asks to export missing translations, untranslated strings, i18n diffs, or locale-file changes.
|
||||
---
|
||||
|
||||
# translation diff export
|
||||
|
||||
Use this skill when you need a small UniGetUI translation patch instead of sending a full language JSON file for review or translation.
|
||||
|
||||
It exports only the active untranslated or source-changed strings, creates the translator handoff files, and prepares the patch set needed by `translation-diff-translate` and `translation-diff-import`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- PowerShell 7 (`pwsh`).
|
||||
- `git` available on `PATH`.
|
||||
- `cirup` available on `PATH`.
|
||||
|
||||
When source strings changed, run `translation-source-sync` first so `lang_en.json` reflects current source usage before exporting downstream translation patches.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/export-translation-diff.ps1`: Exports JSON patch artifacts and generates a translation handoff prompt.
|
||||
- `scripts/test-translation-diff.ps1`: Runs a local end-to-end smoke test against the checked-in UniGetUI language files.
|
||||
|
||||
Use this skill's wrapper scripts first; the downstream translate and import steps are documented in [translation-diff-translate](../translation-diff-translate/SKILL.md) and [translation-diff-import](../translation-diff-import/SKILL.md).
|
||||
|
||||
## Usage
|
||||
|
||||
Export untranslated French strings only:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-diff-export/scripts/export-translation-diff.ps1 \
|
||||
-NeutralJson ./src/Languages/lang_en.json \
|
||||
-TargetJson ./src/Languages/lang_fr.json \
|
||||
-Language fr
|
||||
```
|
||||
|
||||
Export untranslated French strings plus English source values changed since `origin/main`:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-diff-export/scripts/export-translation-diff.ps1 \
|
||||
-NeutralJson ./src/Languages/lang_en.json \
|
||||
-TargetJson ./src/Languages/lang_fr.json \
|
||||
-Language fr \
|
||||
-BaseRef origin/main
|
||||
```
|
||||
|
||||
Optional parameters:
|
||||
|
||||
- `-OutputDir` (default: `generated/translation-diff-export`)
|
||||
- `-KeepIntermediate`
|
||||
|
||||
Run the built-in smoke test:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-diff-export/scripts/test-translation-diff.ps1
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
For `lang_en.json` and language `fr`, the script generates:
|
||||
|
||||
- `generated/translation-diff-export/lang.diff.fr.source.json`
|
||||
- `generated/translation-diff-export/lang.diff.fr.translated.json`
|
||||
- `generated/translation-diff-export/lang.diff.fr.reference.json`
|
||||
- `generated/translation-diff-export/lang.diff.fr.prompt.md`
|
||||
|
||||
If `-KeepIntermediate` is used, git-baseline snapshots are kept under `generated/translation-diff-export/tmp/`.
|
||||
|
||||
The smoke test writes its temporary artifacts under `generated/translation-diff-export-demo/`.
|
||||
|
||||
## Validate The Export
|
||||
|
||||
After export, confirm the patch is usable before handing it off:
|
||||
|
||||
1. Check that `.source.json` is not empty unless you expected no work for that language.
|
||||
2. Confirm `.translated.json` is sparse and does not contain copied English placeholders for unfinished keys.
|
||||
3. If the patch should include recent English changes, rerun with `-BaseRef` and compare the resulting `.source.json`.
|
||||
4. Run the smoke test if you changed the export workflow itself.
|
||||
|
||||
## Hand Off To Translate
|
||||
|
||||
After exporting the patch, use the generated `.prompt.md` file with [translation-diff-translate](../translation-diff-translate/SKILL.md) to update the sparse translated working copy.
|
||||
|
||||
## Hand Off To Import
|
||||
|
||||
After translating the patch, merge it back into the full language file with [translation-diff-import](../translation-diff-import/SKILL.md):
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-diff-import/scripts/import-translation-diff.ps1 \
|
||||
-TranslatedPatch ./generated/translation-diff-export/lang.diff.fr.translated.json \
|
||||
-SourcePatch ./generated/translation-diff-export/lang.diff.fr.source.json \
|
||||
-TargetJson ./src/Languages/lang_fr.json \
|
||||
-NeutralJson ./src/Languages/lang_en.json \
|
||||
-OutputJson ./src/Languages/lang_fr.merged.json
|
||||
```
|
||||
|
||||
Keep the `.translated.json` file sparse. If a key is not translated yet, leave it out instead of copying the English source value into the working copy.
|
||||
@@ -0,0 +1,3 @@
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
. (Join-Path $PSScriptRoot '..\..\..\..\scripts\translation\Languages\TranslationJsonTools.ps1')
|
||||
@@ -0,0 +1,508 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$NeutralJson,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TargetJson,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Language,
|
||||
|
||||
[string]$BaseRef,
|
||||
|
||||
[string]$OutputDir = 'generated/translation-diff-export',
|
||||
|
||||
[switch]$ActiveOnly,
|
||||
|
||||
[switch]$KeepIntermediate
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot 'TranslationDiff.JsonTools.ps1')
|
||||
|
||||
function Get-GitJsonMap {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepositoryRoot,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BaseRef,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepoRelativePath
|
||||
)
|
||||
|
||||
$gitSpec = '{0}:{1}' -f $BaseRef, $RepoRelativePath
|
||||
$rawContent = & git -C $RepositoryRoot show $gitSpec 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$jsonText = [string]::Join([Environment]::NewLine, @($rawContent))
|
||||
if ([string]::IsNullOrWhiteSpace($jsonText)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$tempPath = Join-Path $env:TEMP ('translation-diff-{0}.json' -f [System.Guid]::NewGuid().ToString('N'))
|
||||
try {
|
||||
New-Utf8File -Path $tempPath -Content $jsonText
|
||||
return Read-OrderedJsonMap -Path $tempPath
|
||||
}
|
||||
finally {
|
||||
Remove-Item -Path $tempPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Get-CirupKeySet {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Arguments
|
||||
)
|
||||
|
||||
$rows = Invoke-CirupJson -Arguments $Arguments
|
||||
$keys = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::Ordinal)
|
||||
foreach ($row in $rows) {
|
||||
if (-not ($row -is [System.Collections.IDictionary]) -or -not $row.Contains('name')) {
|
||||
throw 'cirup returned an unexpected JSON payload.'
|
||||
}
|
||||
|
||||
[void]$keys.Add([string]$row['name'])
|
||||
}
|
||||
|
||||
return $keys
|
||||
}
|
||||
|
||||
function Get-EmptyTargetKeys {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$SourceMap,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$TargetMap
|
||||
)
|
||||
|
||||
$keys = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::Ordinal)
|
||||
foreach ($entry in $SourceMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if (-not $TargetMap.Contains($key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace([string]$TargetMap[$key])) {
|
||||
[void]$keys.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
return $keys
|
||||
}
|
||||
|
||||
function Test-IntentionalSourceEqualValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LanguageCode,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$SourceValue,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[AllowEmptyString()]
|
||||
[string]$TargetValue = ''
|
||||
)
|
||||
|
||||
if ($SourceValue -in @(
|
||||
'OK',
|
||||
'UniGetUI',
|
||||
'UniGetUI - {0} {1}'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'it' -and $SourceValue -ceq 'No' -and $TargetValue -ceq 'No') {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'ca' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'1 - Errors',
|
||||
'Error',
|
||||
'Global',
|
||||
'Local',
|
||||
'Manifest',
|
||||
'Manifests',
|
||||
'No',
|
||||
'Notes:',
|
||||
'Text'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'pt_PT' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Global',
|
||||
'Local'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'hr' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Manifest',
|
||||
'Status',
|
||||
'URL'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'es' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Error',
|
||||
'Global',
|
||||
'Local',
|
||||
'No',
|
||||
'URL'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'fr' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Ascendant',
|
||||
'Descendant',
|
||||
'Global',
|
||||
'Local',
|
||||
'Portable',
|
||||
'Source',
|
||||
'Sources',
|
||||
'Verbose',
|
||||
'Version',
|
||||
'installation',
|
||||
'option',
|
||||
'version {0}',
|
||||
'{0} minutes'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'nl' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'1 week',
|
||||
'Filters',
|
||||
'Manifest',
|
||||
'Status',
|
||||
'Updates',
|
||||
'URL',
|
||||
'update',
|
||||
'website',
|
||||
'{0} status'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'sk' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Manifest',
|
||||
'Text'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'de' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Global',
|
||||
'Manifest',
|
||||
'Name',
|
||||
'Repository',
|
||||
'Start',
|
||||
'Status',
|
||||
'Text',
|
||||
'Updates',
|
||||
'URL',
|
||||
'Verbose',
|
||||
'Version',
|
||||
'Version:',
|
||||
'optional',
|
||||
'{package} Installation',
|
||||
'{package} Update'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'sv' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Global',
|
||||
'Manifest',
|
||||
'Start',
|
||||
'Status',
|
||||
'Text',
|
||||
'Version',
|
||||
'Version:',
|
||||
'installation',
|
||||
'version {0}',
|
||||
'{0} installation',
|
||||
'{0} status',
|
||||
'{pm} version:'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'id' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Global',
|
||||
'Grid',
|
||||
'Manifest',
|
||||
'Status',
|
||||
'URL',
|
||||
'Verbose',
|
||||
'{0} status'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'fil' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Android Subsystem',
|
||||
'Default',
|
||||
'Error',
|
||||
'Global',
|
||||
'Grid',
|
||||
'Machine | Global',
|
||||
'Manifest',
|
||||
'OK',
|
||||
'Ok',
|
||||
'Package',
|
||||
'Password',
|
||||
'Portable',
|
||||
'Portable mode',
|
||||
'PreRelease',
|
||||
'Repository',
|
||||
'Source',
|
||||
'Source:',
|
||||
'Telemetry',
|
||||
'Text',
|
||||
'URL',
|
||||
'UniGetUI',
|
||||
'UniGetUI - {0} {1}',
|
||||
'User',
|
||||
'Username',
|
||||
'Verbose',
|
||||
'website',
|
||||
'library'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
. ([System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..\..\scripts\translation\Languages\IntentionalSourceEqualValues.ps1')))
|
||||
|
||||
function Sync-TranslatedWorkingCopy {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LanguageCode,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$CurrentSourceMap,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TranslatedPatchPath,
|
||||
|
||||
[System.Collections.IDictionary]$PreviousSourceMap
|
||||
)
|
||||
|
||||
$translatedMap = Read-OrderedJsonMap -Path $TranslatedPatchPath
|
||||
$syncedMap = New-OrderedStringMap
|
||||
|
||||
foreach ($entry in $CurrentSourceMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
$sourceValue = [string]$entry.Value
|
||||
if (-not $translatedMap.Contains($key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$translatedValue = [string]$translatedMap[$key]
|
||||
if ([string]::IsNullOrWhiteSpace($translatedValue)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($translatedValue -ceq $sourceValue) {
|
||||
if (-not (Test-IntentionalSourceEqualValue -LanguageCode $LanguageCode -SourceValue $sourceValue -TargetValue $translatedValue)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $PreviousSourceMap -and $PreviousSourceMap.Contains($key)) {
|
||||
if ($translatedValue -ceq [string]$PreviousSourceMap[$key]) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
$syncedMap[$key] = $translatedValue
|
||||
}
|
||||
|
||||
Write-OrderedJsonMap -Path $TranslatedPatchPath -Map $syncedMap
|
||||
}
|
||||
|
||||
function Get-FilteredPatchMap {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$Map,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$NeutralMap,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[switch]$ActiveOnly
|
||||
)
|
||||
|
||||
if (-not $ActiveOnly.IsPresent) {
|
||||
return $Map
|
||||
}
|
||||
|
||||
$activeKeys = Split-TranslationMapAtBoundary -Map $NeutralMap
|
||||
$filteredMap = New-OrderedStringMap
|
||||
foreach ($entry in $activeKeys.ActiveMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if ($Map.Contains($key)) {
|
||||
$filteredMap[$key] = [string]$Map[$key]
|
||||
}
|
||||
}
|
||||
|
||||
return $filteredMap
|
||||
}
|
||||
|
||||
Assert-Command -Name 'git'
|
||||
Assert-Command -Name 'cirup'
|
||||
|
||||
$neutralPath = Get-FullPath -Path $NeutralJson
|
||||
$targetPath = Get-FullPath -Path $TargetJson
|
||||
if (-not (Test-Path -Path $neutralPath -PathType Leaf)) {
|
||||
throw "Neutral JSON file not found: $neutralPath"
|
||||
}
|
||||
|
||||
$languageCode = $Language.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($languageCode)) {
|
||||
throw 'Language must not be empty.'
|
||||
}
|
||||
|
||||
$languagesReferencePath = Get-LanguagesReferencePath -NeutralJsonPath $neutralPath
|
||||
Assert-LanguageCodeKnown -LanguagesReferencePath $languagesReferencePath -LanguageCode $languageCode
|
||||
|
||||
$outputRoot = Get-FullPath -Path $OutputDir
|
||||
$tmpRoot = Join-Path $outputRoot 'tmp'
|
||||
New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null
|
||||
New-Item -Path $tmpRoot -ItemType Directory -Force | Out-Null
|
||||
|
||||
$cirupTargetPath = $targetPath
|
||||
if (-not (Test-Path -Path $targetPath -PathType Leaf)) {
|
||||
$cirupTargetPath = Join-Path $tmpRoot 'target.empty.json'
|
||||
Write-OrderedJsonMap -Path $cirupTargetPath -Map (New-OrderedStringMap)
|
||||
}
|
||||
|
||||
$baseName = Get-PatchBaseName -NeutralJsonPath $neutralPath
|
||||
$sourcePatchPath = Join-Path $outputRoot ('{0}.diff.{1}.source.json' -f $baseName, $languageCode)
|
||||
$translatedPatchPath = Join-Path $outputRoot ('{0}.diff.{1}.translated.json' -f $baseName, $languageCode)
|
||||
$referencePatchPath = Join-Path $outputRoot ('{0}.diff.{1}.reference.json' -f $baseName, $languageCode)
|
||||
$promptPath = Join-Path $outputRoot ('{0}.diff.{1}.prompt.md' -f $baseName, $languageCode)
|
||||
|
||||
$neutralMap = Read-OrderedJsonMap -Path $neutralPath
|
||||
$targetMap = Read-OrderedJsonMap -Path $targetPath
|
||||
$baseSummary = 'No git baseline was provided. The patch contains untranslated UniGetUI entries only.'
|
||||
$changedSourceKeys = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::Ordinal)
|
||||
$missingTargetKeys = Get-CirupKeySet -Arguments @('file-diff', $neutralPath, $cirupTargetPath)
|
||||
$rawEqualEnglishKeys = Get-CirupKeySet -Arguments @('file-intersect', $neutralPath, $cirupTargetPath)
|
||||
$equalEnglishKeys = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::Ordinal)
|
||||
$rawEqualEnglishKeys | ForEach-Object {
|
||||
$key = [string]$_
|
||||
if (-not (Test-IntentionalSourceEqualValue -LanguageCode $Language -SourceValue ([string]$neutralMap[$key]) -TargetValue ([string]$targetMap[$key]))) {
|
||||
[void]$equalEnglishKeys.Add($key)
|
||||
}
|
||||
}
|
||||
$emptyTargetKeys = Get-EmptyTargetKeys -SourceMap $neutralMap -TargetMap $targetMap
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($BaseRef)) {
|
||||
$repoRoot = Get-RepositoryRoot -WorkingDirectory (Split-Path -Path $neutralPath -Parent)
|
||||
$repoRelativeNeutralPath = Get-RepoRelativePath -RepositoryRoot $repoRoot -FilePath $neutralPath
|
||||
$baseSourceMap = Get-GitJsonMap -RepositoryRoot $repoRoot -BaseRef $BaseRef -RepoRelativePath $repoRelativeNeutralPath
|
||||
$safeBaseRef = Get-SafeLabel -Value $BaseRef
|
||||
$baseSnapshotPath = Join-Path $tmpRoot ('{0}.baseline.{1}.json' -f $baseName, $safeBaseRef)
|
||||
|
||||
if ($null -ne $baseSourceMap) {
|
||||
Write-OrderedJsonMap -Path $baseSnapshotPath -Map $baseSourceMap
|
||||
$baseSummary = "Git baseline '$BaseRef' was used to include keys that are new or whose English value changed since that revision."
|
||||
}
|
||||
else {
|
||||
Write-OrderedJsonMap -Path $baseSnapshotPath -Map (New-OrderedStringMap)
|
||||
$baseSummary = "Git baseline '$BaseRef' did not contain the neutral JSON file, so all current English entries were treated as new for the changed-source delta."
|
||||
}
|
||||
|
||||
$changedSourceKeys = Get-CirupKeySet -Arguments @('diff-with-base', $baseSnapshotPath, $neutralPath, $neutralPath)
|
||||
}
|
||||
|
||||
$sourcePatchMap = New-OrderedStringMap
|
||||
$referencePatchMap = New-OrderedStringMap
|
||||
|
||||
$keysToTranslate = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::Ordinal)
|
||||
foreach ($key in $missingTargetKeys) {
|
||||
[void]$keysToTranslate.Add([string]$key)
|
||||
}
|
||||
|
||||
foreach ($key in $equalEnglishKeys) {
|
||||
[void]$keysToTranslate.Add([string]$key)
|
||||
}
|
||||
|
||||
foreach ($key in $emptyTargetKeys) {
|
||||
[void]$keysToTranslate.Add([string]$key)
|
||||
}
|
||||
|
||||
foreach ($key in $changedSourceKeys) {
|
||||
[void]$keysToTranslate.Add([string]$key)
|
||||
}
|
||||
|
||||
foreach ($entry in $neutralMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
$sourceValue = [string]$entry.Value
|
||||
|
||||
if ($keysToTranslate.Contains($key)) {
|
||||
$sourcePatchMap[$key] = $sourceValue
|
||||
continue
|
||||
}
|
||||
|
||||
if ($targetMap.Contains($key)) {
|
||||
$targetValue = [string]$targetMap[$key]
|
||||
if (-not [string]::IsNullOrWhiteSpace($targetValue) -and $targetValue -cne $sourceValue) {
|
||||
$referencePatchMap[$key] = $targetValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sourcePatchMap = Get-FilteredPatchMap -Map $sourcePatchMap -NeutralMap $neutralMap -ActiveOnly:$ActiveOnly
|
||||
$referencePatchMap = Get-FilteredPatchMap -Map $referencePatchMap -NeutralMap $neutralMap -ActiveOnly:$ActiveOnly
|
||||
|
||||
if (Test-Path -Path $sourcePatchPath -PathType Leaf) {
|
||||
$previousSourceMap = Read-OrderedJsonMap -Path $sourcePatchPath
|
||||
}
|
||||
else {
|
||||
$previousSourceMap = $null
|
||||
}
|
||||
|
||||
Write-OrderedJsonMap -Path $sourcePatchPath -Map $sourcePatchMap
|
||||
Write-OrderedJsonMap -Path $referencePatchPath -Map $referencePatchMap
|
||||
Sync-TranslatedWorkingCopy -LanguageCode $languageCode -CurrentSourceMap $sourcePatchMap -TranslatedPatchPath $translatedPatchPath -PreviousSourceMap $previousSourceMap
|
||||
|
||||
$mergedTargetPath = Join-Path ([System.IO.Path]::GetDirectoryName($targetPath)) ('{0}.merged.json' -f [System.IO.Path]::GetFileNameWithoutExtension($targetPath))
|
||||
$skillsRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..'))
|
||||
$translateHandoffScript = Join-Path $skillsRoot 'translation-diff-translate\scripts\write-translation-handoff.ps1'
|
||||
$validationScript = Join-Path $skillsRoot 'translation-diff-import\scripts\validate-language-file.ps1'
|
||||
if (-not (Test-Path -Path $translateHandoffScript -PathType Leaf)) {
|
||||
throw "Translation handoff script not found: $translateHandoffScript"
|
||||
}
|
||||
|
||||
& $translateHandoffScript -BaseName $baseName -Language $languageCode -SourcePatch $sourcePatchPath -TranslatedPatch $translatedPatchPath -ReferencePatch $referencePatchPath -TargetJson $targetPath -NeutralJson $neutralPath -MergedTargetJson $mergedTargetPath -ValidationScript $validationScript -OutputPrompt $promptPath -BaseSummary $baseSummary
|
||||
|
||||
if (-not $KeepIntermediate.IsPresent) {
|
||||
Remove-Item -Path $tmpRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Output "Created source patch: $sourcePatchPath"
|
||||
Write-Output "Refreshed translated working copy: $translatedPatchPath"
|
||||
Write-Output "Created reference patch: $referencePatchPath"
|
||||
Write-Output "Created translation handoff prompt: $promptPath"
|
||||
if ($ActiveOnly.IsPresent) {
|
||||
Write-Output 'Patch scope: active translation keys only'
|
||||
}
|
||||
if ($KeepIntermediate.IsPresent) {
|
||||
Write-Output "Kept intermediate files under: $tmpRoot"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
param(
|
||||
[string]$Language = 'fr',
|
||||
|
||||
[string]$OutputRoot = 'generated/translation-diff-export-demo'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot 'TranslationDiff.JsonTools.ps1')
|
||||
|
||||
$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..\..'))
|
||||
$neutralPath = Join-Path $repoRoot 'src\Languages\lang_en.json'
|
||||
$targetPath = Join-Path $repoRoot ('src\Languages\lang_{0}.json' -f $Language)
|
||||
$exportScript = Join-Path $PSScriptRoot 'export-translation-diff.ps1'
|
||||
$importScript = Join-Path $repoRoot '.agents\skills\translation-diff-import\scripts\import-translation-diff.ps1'
|
||||
$validateScript = Join-Path $repoRoot '.agents\skills\translation-diff-import\scripts\validate-language-file.ps1'
|
||||
|
||||
if (-not (Test-Path -Path $neutralPath -PathType Leaf)) {
|
||||
throw "Neutral language file not found: $neutralPath"
|
||||
}
|
||||
|
||||
& $exportScript -NeutralJson $neutralPath -TargetJson $targetPath -Language $Language -OutputDir $OutputRoot
|
||||
|
||||
$baseName = Get-PatchBaseName -NeutralJsonPath $neutralPath
|
||||
$sourcePatchPath = Join-Path (Get-FullPath -Path $OutputRoot) ('{0}.diff.{1}.source.json' -f $baseName, $Language)
|
||||
$translatedPatchPath = Join-Path (Get-FullPath -Path $OutputRoot) ('{0}.diff.{1}.translated.json' -f $baseName, $Language)
|
||||
$mergedTargetPath = Join-Path (Get-FullPath -Path $OutputRoot) ('lang_{0}.merged.smoke.json' -f $Language)
|
||||
|
||||
$sourcePatchMap = Read-OrderedJsonMap -Path $sourcePatchPath
|
||||
if ($sourcePatchMap.Count -eq 0) {
|
||||
throw 'Smoke test patch is empty. Pick a target language with untranslated entries to exercise the workflow.'
|
||||
}
|
||||
|
||||
$translatedPatchMap = New-OrderedStringMap
|
||||
$firstEntry = $sourcePatchMap.GetEnumerator() | Select-Object -First 1
|
||||
$translatedPatchMap[[string]$firstEntry.Key] = '[smoke {0}] {1}' -f $Language, [string]$firstEntry.Value
|
||||
Write-OrderedJsonMap -Path $translatedPatchPath -Map $translatedPatchMap
|
||||
|
||||
& $importScript -TranslatedPatch $translatedPatchPath -SourcePatch $sourcePatchPath -TargetJson $targetPath -NeutralJson $neutralPath -OutputJson $mergedTargetPath
|
||||
|
||||
& $validateScript -NeutralJson $neutralPath -TargetJson $mergedTargetPath -PatchJson $sourcePatchPath
|
||||
|
||||
Write-Output "Smoke test completed successfully. Merged output: $mergedTargetPath"
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: translation-diff-import
|
||||
description: Merges translated key-value pairs from a UniGetUI JSON localization patch back into the full language file and validates the merged result. Use when the user asks to apply, merge, or import a translation patch.
|
||||
---
|
||||
|
||||
# translation diff import
|
||||
|
||||
Use this skill after the export skill produced a `.source.json`, `.translated.json`, and `.reference.json` set and you want to merge the translated working copy back into the full target-language JSON file.
|
||||
|
||||
See [translation-diff-export](../translation-diff-export/SKILL.md) for the patch-generation step that produces the expected inputs.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- PowerShell 7 (`pwsh`).
|
||||
- `cirup` available on `PATH`.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/import-translation-diff.ps1`: Validates and merges a translated patch into the full language file.
|
||||
- `scripts/validate-language-file.ps1`: Validates placeholder, token, HTML-fragment, and newline parity for either a full language file or just the active patch keys against `lang_en.json`.
|
||||
|
||||
## Usage
|
||||
|
||||
Import the translated French working copy from the export skill into the full French JSON file:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-diff-import/scripts/import-translation-diff.ps1 \
|
||||
-TranslatedPatch ./generated/translation-diff-export/lang.diff.fr.translated.json \
|
||||
-SourcePatch ./generated/translation-diff-export/lang.diff.fr.source.json \
|
||||
-TargetJson ./src/Languages/lang_fr.json \
|
||||
-NeutralJson ./src/Languages/lang_en.json \
|
||||
-OutputJson ./src/Languages/lang_fr.merged.json
|
||||
```
|
||||
|
||||
Validate the merged output:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-diff-import/scripts/validate-language-file.ps1 \
|
||||
-NeutralJson ./src/Languages/lang_en.json \
|
||||
-TargetJson ./src/Languages/lang_fr.merged.json \
|
||||
-PatchJson ./generated/translation-diff-export/lang.diff.fr.source.json
|
||||
```
|
||||
|
||||
Optional parameters:
|
||||
|
||||
- `-OutputDir` (default: `generated/translation-diff-import`)
|
||||
- `-AllowUnchangedValues`
|
||||
- `-KeepIntermediate`
|
||||
|
||||
Recommended input mapping from the export skill:
|
||||
|
||||
- `-TranslatedPatch`: `generated/translation-diff-export/lang.diff.fr.translated.json`
|
||||
- `-SourcePatch`: `generated/translation-diff-export/lang.diff.fr.source.json`
|
||||
|
||||
## Output
|
||||
|
||||
- A merged `.json` file that contains both the previously translated entries and the imported patch values.
|
||||
- If `-KeepIntermediate` is used, patch snapshots are preserved under `generated/translation-diff-import/tmp/`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- If the target JSON file does not exist yet, the script can create an output from the translated patch alone.
|
||||
- When `-SourcePatch` is provided, the script validates translated keys, placeholder tokens, HTML-like fragments, newline counts, and likely untranslated values before delegating the merge to `cirup` while allowing missing keys for partial progress.
|
||||
- The import skill expects `.translated.json` to remain sparse. Untranslated keys should be omitted instead of copied in English unless you intentionally bypass that check with `-AllowUnchangedValues`.
|
||||
@@ -0,0 +1,168 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TranslatedPatch,
|
||||
|
||||
[string]$SourcePatch,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TargetJson,
|
||||
|
||||
[string]$NeutralJson,
|
||||
|
||||
[string]$OutputJson,
|
||||
|
||||
[string]$OutputDir = 'generated/translation-diff-import',
|
||||
|
||||
[switch]$AllowUnchangedValues,
|
||||
|
||||
[switch]$KeepIntermediate
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot '..\..\translation-diff-export\scripts\TranslationDiff.JsonTools.ps1')
|
||||
. ([System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..\..\scripts\translation\Languages\IntentionalSourceEqualValues.ps1')))
|
||||
|
||||
function Assert-PatchCompatibility {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$SourceMap,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$TranslatedMap,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LanguageCode,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[bool]$AllowUnchanged
|
||||
)
|
||||
|
||||
foreach ($entry in $TranslatedMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
$translatedValue = [string]$entry.Value
|
||||
if (-not $SourceMap.Contains($key)) {
|
||||
throw "Translated patch contains unexpected key '$key'."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($translatedValue)) {
|
||||
throw "Translated patch contains an empty value for key '$key'."
|
||||
}
|
||||
|
||||
$sourceValue = [string]$SourceMap[$key]
|
||||
Assert-TranslationStructure -SourceValue $sourceValue -TranslatedValue $translatedValue -Key $key
|
||||
|
||||
if (-not $AllowUnchanged -and $translatedValue -ceq $sourceValue -and -not (Test-IntentionalSourceEqualValue -LanguageCode $LanguageCode -SourceValue $sourceValue -TargetValue $translatedValue)) {
|
||||
throw "Translated patch still contains the English source value for key '$key'. Use -AllowUnchangedValues only when that is intentional."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Merge-LanguageMaps {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$NeutralMap,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$TargetMap,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$TranslatedPatchMap
|
||||
)
|
||||
|
||||
$mergedMap = New-OrderedStringMap
|
||||
|
||||
foreach ($entry in $NeutralMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if ($TranslatedPatchMap.Contains($key)) {
|
||||
$mergedMap[$key] = [string]$TranslatedPatchMap[$key]
|
||||
continue
|
||||
}
|
||||
|
||||
if ($TargetMap.Contains($key)) {
|
||||
$mergedMap[$key] = [string]$TargetMap[$key]
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($entry in $TargetMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if (-not $NeutralMap.Contains($key) -and -not $mergedMap.Contains($key)) {
|
||||
$mergedMap[$key] = [string]$entry.Value
|
||||
}
|
||||
}
|
||||
|
||||
return $mergedMap
|
||||
}
|
||||
|
||||
$translatedPatchPath = Get-FullPath -Path $TranslatedPatch
|
||||
if (-not (Test-Path -Path $translatedPatchPath -PathType Leaf)) {
|
||||
throw "Translated patch file not found: $translatedPatchPath"
|
||||
}
|
||||
|
||||
$targetPath = Get-FullPath -Path $TargetJson
|
||||
$targetMap = Read-OrderedJsonMap -Path $targetPath
|
||||
$translatedPatchMap = Read-OrderedJsonMap -Path $translatedPatchPath
|
||||
$targetBaseName = [System.IO.Path]::GetFileNameWithoutExtension($targetPath)
|
||||
$languageCode = if ($targetBaseName.StartsWith('lang_', [System.StringComparison]::Ordinal)) {
|
||||
$targetBaseName.Substring(5)
|
||||
}
|
||||
else {
|
||||
''
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($NeutralJson)) {
|
||||
$NeutralJson = Join-Path (Split-Path -Path $targetPath -Parent) 'lang_en.json'
|
||||
}
|
||||
|
||||
$neutralPath = Get-FullPath -Path $NeutralJson
|
||||
if (-not (Test-Path -Path $neutralPath -PathType Leaf)) {
|
||||
throw "Neutral JSON file not found: $neutralPath"
|
||||
}
|
||||
|
||||
$neutralMap = Read-OrderedJsonMap -Path $neutralPath
|
||||
$sourceMap = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($SourcePatch)) {
|
||||
$sourcePatchPath = Get-FullPath -Path $SourcePatch
|
||||
if (-not (Test-Path -Path $sourcePatchPath -PathType Leaf)) {
|
||||
throw "Source patch file not found: $sourcePatchPath"
|
||||
}
|
||||
|
||||
$sourceMap = Read-OrderedJsonMap -Path $sourcePatchPath
|
||||
Assert-PatchCompatibility -SourceMap $sourceMap -TranslatedMap $translatedPatchMap -LanguageCode $languageCode -AllowUnchanged:$AllowUnchangedValues.IsPresent
|
||||
}
|
||||
|
||||
foreach ($entry in $translatedPatchMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if (-not $neutralMap.Contains($key)) {
|
||||
throw "Translated patch key '$key' does not exist in the neutral language file."
|
||||
}
|
||||
}
|
||||
|
||||
$outputRoot = Get-FullPath -Path $OutputDir
|
||||
$tmpRoot = Join-Path $outputRoot 'tmp'
|
||||
New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null
|
||||
New-Item -Path $tmpRoot -ItemType Directory -Force | Out-Null
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($OutputJson)) {
|
||||
if (Test-Path -Path $targetPath -PathType Leaf) {
|
||||
$targetName = [System.IO.Path]::GetFileNameWithoutExtension($targetPath)
|
||||
$OutputJson = Join-Path (Split-Path -Path $targetPath -Parent) ('{0}.merged.json' -f $targetName)
|
||||
}
|
||||
else {
|
||||
$OutputJson = $targetPath
|
||||
}
|
||||
}
|
||||
|
||||
$outputJsonPath = Get-FullPath -Path $OutputJson
|
||||
$mergedMap = Merge-LanguageMaps -NeutralMap $neutralMap -TargetMap $targetMap -TranslatedPatchMap $translatedPatchMap
|
||||
Write-OrderedJsonMap -Path $outputJsonPath -Map $mergedMap
|
||||
|
||||
if (-not $KeepIntermediate.IsPresent) {
|
||||
Remove-Item -Path $tmpRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Output "Merged translated patch into: $outputJsonPath"
|
||||
if ($null -ne $sourceMap) {
|
||||
Write-Output "Validated translated patch against: $SourcePatch"
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$NeutralJson,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TargetJson,
|
||||
|
||||
[string]$PatchJson
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot '..\..\translation-diff-export\scripts\TranslationDiff.JsonTools.ps1')
|
||||
|
||||
$neutralPath = Get-FullPath -Path $NeutralJson
|
||||
$targetPath = Get-FullPath -Path $TargetJson
|
||||
|
||||
if (-not (Test-Path -Path $neutralPath -PathType Leaf)) {
|
||||
throw "Neutral JSON file not found: $neutralPath"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $targetPath -PathType Leaf)) {
|
||||
throw "Target JSON file not found: $targetPath"
|
||||
}
|
||||
|
||||
$neutralMap = Read-OrderedJsonMap -Path $neutralPath
|
||||
$targetMap = Read-OrderedJsonMap -Path $targetPath
|
||||
|
||||
$keysToValidate = New-Object System.Collections.Generic.List[string]
|
||||
if (-not [string]::IsNullOrWhiteSpace($PatchJson)) {
|
||||
$patchPath = Get-FullPath -Path $PatchJson
|
||||
if (-not (Test-Path -Path $patchPath -PathType Leaf)) {
|
||||
throw "Patch JSON file not found: $patchPath"
|
||||
}
|
||||
|
||||
$patchMap = Read-OrderedJsonMap -Path $patchPath
|
||||
foreach ($entry in $patchMap.GetEnumerator()) {
|
||||
$keysToValidate.Add([string]$entry.Key)
|
||||
}
|
||||
}
|
||||
else {
|
||||
foreach ($entry in $targetMap.GetEnumerator()) {
|
||||
$keysToValidate.Add([string]$entry.Key)
|
||||
}
|
||||
}
|
||||
|
||||
$validatedCount = 0
|
||||
foreach ($key in $keysToValidate) {
|
||||
if (-not $neutralMap.Contains($key)) {
|
||||
throw "Target file contains key '$key' that does not exist in the neutral language file."
|
||||
}
|
||||
|
||||
if (-not $targetMap.Contains($key)) {
|
||||
throw "Target file does not contain required key '$key'."
|
||||
}
|
||||
|
||||
$targetValue = [string]$targetMap[$key]
|
||||
Assert-TranslationStructure -SourceValue ([string]$neutralMap[$key]) -TranslatedValue $targetValue -Key $key
|
||||
$validatedCount += 1
|
||||
}
|
||||
|
||||
Write-Output "Validated $validatedCount translated entries against: $neutralPath"
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: translation-diff-translate
|
||||
description: Translates a sparse UniGetUI JSON language patch, writes completed entries into the working copy, preserves placeholders and terminology, and prepares the patch for merge-back. Use when the user asks to translate or localize i18n strings, localization JSON, or language-file patches.
|
||||
---
|
||||
|
||||
# translation diff translate
|
||||
|
||||
Use this skill after `translation-diff-export` produced a `.source.json`, `.translated.json`, and `.reference.json` set and you want to update the sparse translated working copy.
|
||||
|
||||
It translates only the active patch keys, keeps `.translated.json` as the sole writable file, preserves placeholders and formatting, and hands the completed working copy to `translation-diff-import` for merge-back.
|
||||
|
||||
## Execution Expectations
|
||||
|
||||
1. Edit `.translated.json` directly. Do not create helper scripts, temporary reports, or coverage probes.
|
||||
2. Do not install packages or search for external translation services unless the user explicitly asks for automation.
|
||||
3. If `.translated.json` is empty, that is expected. Translate from `.source.json` into `.translated.json`.
|
||||
4. Use `.reference.json` only as terminology guidance, not as a reason to pause and analyze the rest of the repository.
|
||||
5. If the patch is too large to finish reasonably in one pass, report that constraint to the user instead of creating automation on your own.
|
||||
|
||||
## Script
|
||||
|
||||
- `scripts/write-translation-handoff.ps1`: Writes a small `.prompt.md` file that points an agent at this skill with the concrete patch paths created by the export skill.
|
||||
|
||||
## Usage
|
||||
|
||||
Generate a handoff prompt for a translated French working copy:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-diff-translate/scripts/write-translation-handoff.ps1 \
|
||||
-BaseName lang \
|
||||
-Language fr \
|
||||
-SourcePatch ./generated/translation-diff-export/lang.diff.fr.source.json \
|
||||
-TranslatedPatch ./generated/translation-diff-export/lang.diff.fr.translated.json \
|
||||
-ReferencePatch ./generated/translation-diff-export/lang.diff.fr.reference.json \
|
||||
-TargetJson ./src/Languages/lang_fr.json \
|
||||
-NeutralJson ./src/Languages/lang_en.json \
|
||||
-MergedTargetJson ./src/Languages/lang_fr.merged.json \
|
||||
-ValidationScript ./.agents/skills/translation-diff-import/scripts/validate-language-file.ps1 \
|
||||
-OutputPrompt ./generated/translation-diff-export/lang.diff.fr.prompt.md
|
||||
```
|
||||
|
||||
## Translation Rules
|
||||
|
||||
1. Do not edit `.source.json`.
|
||||
2. Write completed translations only into `.translated.json`.
|
||||
3. If a key is not translated yet, omit it from `.translated.json` instead of copying the English source value.
|
||||
4. Preserve key names exactly as-is.
|
||||
5. Preserve placeholders, named tokens, HTML-like fragments, escape sequences, and line breaks.
|
||||
6. Keep translated entries in the same key order as `.source.json` when adding new entries.
|
||||
7. Use `.reference.json` to match existing terminology and tone in the destination language.
|
||||
8. Do not introduce keys that are not present in the source patch.
|
||||
|
||||
## After Translation
|
||||
|
||||
Merge the sparse working copy back into the full destination-language file:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-diff-import/scripts/import-translation-diff.ps1 \
|
||||
-TranslatedPatch ./generated/translation-diff-export/lang.diff.fr.translated.json \
|
||||
-SourcePatch ./generated/translation-diff-export/lang.diff.fr.source.json \
|
||||
-TargetJson ./src/Languages/lang_fr.json \
|
||||
-NeutralJson ./src/Languages/lang_en.json \
|
||||
-OutputJson ./src/Languages/lang_fr.merged.json
|
||||
|
||||
pwsh ./.agents/skills/translation-diff-import/scripts/validate-language-file.ps1 \
|
||||
-NeutralJson ./src/Languages/lang_en.json \
|
||||
-TargetJson ./src/Languages/lang_fr.merged.json \
|
||||
-PatchJson ./generated/translation-diff-export/lang.diff.fr.source.json
|
||||
```
|
||||
@@ -0,0 +1,113 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BaseName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Language,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourcePatch,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TranslatedPatch,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ReferencePatch,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TargetJson,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$NeutralJson,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$MergedTargetJson,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ValidationScript,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputPrompt,
|
||||
|
||||
[string]$BaseSummary = 'No git baseline was provided. The patch contains untranslated UniGetUI entries only.'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function New-Utf8File {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Content
|
||||
)
|
||||
|
||||
$directory = Split-Path -Path $Path -Parent
|
||||
if (-not [string]::IsNullOrWhiteSpace($directory)) {
|
||||
New-Item -Path $directory -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$encoding = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText($Path, $Content, $encoding)
|
||||
}
|
||||
|
||||
$promptPath = [System.IO.Path]::GetFullPath($OutputPrompt)
|
||||
$promptDirectory = Split-Path -Path $promptPath -Parent
|
||||
if (-not [string]::IsNullOrWhiteSpace($promptDirectory)) {
|
||||
New-Item -Path $promptDirectory -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$promptContent = @"
|
||||
# Translate $BaseName to $Language
|
||||
|
||||
## Use Skill
|
||||
|
||||
Use the translation-diff-translate skill for the translation step.
|
||||
|
||||
Detailed workflow and validation rules live in:
|
||||
|
||||
- ./.agents/skills/translation-diff-translate/SKILL.md
|
||||
|
||||
## Context
|
||||
|
||||
- Source patch file: $SourcePatch
|
||||
- Translated working copy: $TranslatedPatch
|
||||
- Reference translations: $ReferencePatch
|
||||
- Full target language file: $TargetJson
|
||||
- Neutral language file: $NeutralJson
|
||||
- $BaseSummary
|
||||
|
||||
## Translation Task
|
||||
|
||||
1. Update only the translated working copy.
|
||||
2. If the translated working copy is empty, translate directly from the source patch into it.
|
||||
3. Keep the translated working copy sparse by omitting keys that are not translated yet.
|
||||
4. Use the reference translations file for terminology, style, and consistency.
|
||||
5. Preserve placeholders, named tokens, HTML-like fragments, escape sequences, and line breaks.
|
||||
6. Do not edit the source patch file.
|
||||
7. Do not write helper scripts, temporary reports, coverage checks, or automation for this task.
|
||||
8. Do not search for external translation tools or install packages unless the user explicitly asks for automation.
|
||||
9. Start translating immediately instead of analyzing repo-wide translation coverage.
|
||||
|
||||
## Expected Action
|
||||
|
||||
- Read the source patch and reference file.
|
||||
- Edit the translated patch file directly.
|
||||
- Add translated JSON entries in source order.
|
||||
- Leave untranslated keys out.
|
||||
- When finished, use the import and validation commands below.
|
||||
|
||||
## Import After Translation
|
||||
|
||||
~~~powershell
|
||||
pwsh ./.agents/skills/translation-diff-import/scripts/import-translation-diff.ps1 -TranslatedPatch "$TranslatedPatch" -SourcePatch "$SourcePatch" -TargetJson "$TargetJson" -NeutralJson "$NeutralJson" -OutputJson "$MergedTargetJson"
|
||||
pwsh "$ValidationScript" -NeutralJson "$NeutralJson" -TargetJson "$MergedTargetJson" -PatchJson "$SourcePatch"
|
||||
~~~
|
||||
|
||||
The import skill validates the translated working copy against the source patch, merges it into the full target file in English key order, and the validation script checks placeholders, HTML-like fragments, and line-break parity.
|
||||
"@
|
||||
|
||||
New-Utf8File -Path $promptPath -Content $promptContent.TrimStart()
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
name: translation-review
|
||||
description: Reviews UniGetUI .json language files for localization quality, detects parity issues, English-equal entries, wrong-script content, and cross-language outliers, then generates a dataset for LLM-assisted linguistic review. Use when the user asks to review translations, audit localization, inspect i18n or l10n quality, or validate language files.
|
||||
---
|
||||
|
||||
# translation review
|
||||
|
||||
Use this skill when you need a quality review of an existing UniGetUI translation, not just coverage, but correctness, language integrity, and consistency against related languages.
|
||||
|
||||
Typical triggers: review translations, check translation quality, mistranslations, wrong language in translation, untranslated strings, localization QA, translation audit, spot-check a language file.
|
||||
|
||||
## Scope
|
||||
|
||||
- Mechanically detect placeholder, HTML-fragment, and newline mismatches.
|
||||
- Flag entries whose translation still equals the English source in a non-English language file.
|
||||
- Detect wrong-script entries for non-Latin languages such as `ua`, `zh_CN`, `ar`, `ko`, `ja`, and `he`.
|
||||
- Build a cross-language comparison table so the reviewing agent can identify outliers, suspicious wording, and wrong-language content.
|
||||
- The agent performs the final linguistic review using the generated report.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- PowerShell 7 (`pwsh`).
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/review-translation-json.ps1`: Generate the review dataset for one UniGetUI language JSON file.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Generate the review dataset for the target language.
|
||||
2. Confirm the output file exists and contains the expected flagged sections or JSON fields.
|
||||
3. If the script fails or the output is empty, validate the target JSON with `pwsh ./scripts/translation/Verify-Translations.ps1` and rerun the review.
|
||||
4. Read the flagged sections first: parity issues, English-equal entries, and wrong-script entries.
|
||||
5. Spot-check the cross-language comparison table, focusing on labels, buttons, settings text, and error messages.
|
||||
6. Report findings inline with the English source text, observed problem, and suggested correction.
|
||||
|
||||
## Usage
|
||||
|
||||
Generate a Markdown review report for French:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-review/scripts/review-translation-json.ps1 \
|
||||
-TargetJson ./src/Languages/lang_fr.json \
|
||||
-Language fr
|
||||
```
|
||||
|
||||
Generate a report for Ukrainian and include script-detection checks:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-review/scripts/review-translation-json.ps1 \
|
||||
-TargetJson ./src/Languages/lang_ua.json \
|
||||
-Language ua
|
||||
```
|
||||
|
||||
Generate a flagged-only JSON report with no cross-language table:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-review/scripts/review-translation-json.ps1 \
|
||||
-TargetJson ./src/Languages/lang_de.json \
|
||||
-Language de \
|
||||
-OutputFormat Json \
|
||||
-FlaggedOnly
|
||||
```
|
||||
|
||||
Optional parameters:
|
||||
|
||||
- `-NeutralJson` — defaults to `src/Languages/lang_en.json`
|
||||
- `-ComparisonLanguages` — languages to include in the comparison table; defaults to other checked-in `lang_*.json` files
|
||||
- `-OutputPath` — defaults to `generated/translation-review/review.<lang>.md`
|
||||
- `-OutputFormat` — `Markdown` (default) or `Json`
|
||||
- `-FlaggedOnly` — skip the full cross-language table and emit only mechanically flagged entries
|
||||
|
||||
## Output
|
||||
|
||||
The Markdown report is structured as:
|
||||
|
||||
1. Summary
|
||||
2. Parity Issues
|
||||
3. English-Equal Entries
|
||||
4. Wrong Script
|
||||
5. Cross-Language Comparison
|
||||
|
||||
Default output path: `generated/translation-review/review.<lang>.md`
|
||||
|
||||
## Agent Review Guidelines
|
||||
|
||||
### Flagged sections
|
||||
|
||||
- Parity issues: confirm whether the placeholder, tag, or newline mismatch is a genuine error or an acceptable localized variant.
|
||||
- English-equal entries: decide whether the English value is an acceptable technical or brand string, or whether it should be translated.
|
||||
- Wrong script: almost always an error for non-Latin languages. Verify against comparison languages and flag for re-translation when confirmed.
|
||||
|
||||
### Cross-language comparison table
|
||||
|
||||
- Sample at least 20–30 rows and prefer entries that look like UI labels, buttons, dialogs, settings descriptions, or error messages.
|
||||
- Look for wrong language, suspiciously different meaning, missing words, or copied English that should probably be translated.
|
||||
- Ignore obvious brand names and technical identifiers.
|
||||
|
||||
### Output format for findings
|
||||
|
||||
Report each issue in this format:
|
||||
|
||||
```text
|
||||
**Source**: `Some English source text`
|
||||
**Problem**: [describe the issue]
|
||||
**Current value**: [the problematic translation]
|
||||
**Suggested correction**: [corrected value, if confident]
|
||||
**Confidence**: High / Medium / Low
|
||||
```
|
||||
|
||||
Group findings by type: parity, English-equal, wrong language, wrong script, or other.
|
||||
|
||||
## Integration with Other Skills
|
||||
|
||||
- Use [translation-status](../translation-status/SKILL.md) first if you need a coverage snapshot before reviewing quality.
|
||||
- Use [translation-diff-export](../translation-diff-export/SKILL.md) to produce a sparse patch for entries that need re-translation after review.
|
||||
- Use [translation-diff-import](../translation-diff-import/SKILL.md) to merge corrected entries back into the full language file.
|
||||
@@ -0,0 +1,435 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$TargetJson,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Language,
|
||||
|
||||
[string]$NeutralJson,
|
||||
[string]$OutputPath,
|
||||
[string[]]$ComparisonLanguages,
|
||||
|
||||
[ValidateSet('Markdown', 'Json')]
|
||||
[string]$OutputFormat = 'Markdown',
|
||||
|
||||
[switch]$FlaggedOnly
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..\..'))
|
||||
. (Join-Path $repoRoot 'scripts\translation\Languages\TranslationJsonTools.ps1')
|
||||
|
||||
function Get-TranslationReviewLanguageReferencePath {
|
||||
return Join-Path $repoRoot 'src\Languages\Data\LanguagesReference.json'
|
||||
}
|
||||
|
||||
function Get-TranslationReviewLanguagesDirectory {
|
||||
return Join-Path $repoRoot 'src\Languages'
|
||||
}
|
||||
|
||||
function Get-TranslationReviewLanguageReference {
|
||||
return Read-OrderedJsonMap -Path (Get-TranslationReviewLanguageReferencePath)
|
||||
}
|
||||
|
||||
function Get-TranslationReviewDisplayLanguage {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LanguageCode,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$LanguageReference
|
||||
)
|
||||
|
||||
if ($LanguageReference.Contains($LanguageCode)) {
|
||||
return [string]$LanguageReference[$LanguageCode]
|
||||
}
|
||||
|
||||
return $LanguageCode
|
||||
}
|
||||
|
||||
function Test-TranslationReviewTechnicalToken {
|
||||
param(
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Value)) { return $false }
|
||||
if ($Value -match '^\d[\d\s%:./-]*$') { return $true }
|
||||
if ($Value -match '^[A-Z0-9]{1,12}$') { return $true }
|
||||
if ($Value -match '^https?://') { return $true }
|
||||
if ($Value -match '^[A-Z][a-zA-Z0-9]+$' -and $Value.Length -le 30) { return $true }
|
||||
if ($Value.Length -le 2) { return $true }
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-TranslationReviewScriptInfo {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LanguageCode
|
||||
)
|
||||
|
||||
switch ($LanguageCode) {
|
||||
'ar' { return @{ Name = 'Arabic'; Pattern = '[\u0600-\u06ff]' } }
|
||||
'be' { return @{ Name = 'Cyrillic'; Pattern = '[\u0400-\u04ff]' } }
|
||||
'bg' { return @{ Name = 'Cyrillic'; Pattern = '[\u0400-\u04ff]' } }
|
||||
'bn' { return @{ Name = 'Bengali'; Pattern = '[\u0980-\u09ff]' } }
|
||||
'el' { return @{ Name = 'Greek'; Pattern = '[\u0370-\u03ff]' } }
|
||||
'fa' { return @{ Name = 'Arabic'; Pattern = '[\u0600-\u06ff]' } }
|
||||
'he' { return @{ Name = 'Hebrew'; Pattern = '[\u0590-\u05ff]' } }
|
||||
'hi' { return @{ Name = 'Devanagari'; Pattern = '[\u0900-\u097f]' } }
|
||||
'ja' { return @{ Name = 'Japanese'; Pattern = '[\u3040-\u30ff\u4e00-\u9fff]' } }
|
||||
'ka' { return @{ Name = 'Georgian'; Pattern = '[\u10a0-\u10ff]' } }
|
||||
'kn' { return @{ Name = 'Kannada'; Pattern = '[\u0c80-\u0cff]' } }
|
||||
'ko' { return @{ Name = 'Korean'; Pattern = '[\uac00-\ud7a3]' } }
|
||||
'mk' { return @{ Name = 'Cyrillic'; Pattern = '[\u0400-\u04ff]' } }
|
||||
'mr' { return @{ Name = 'Devanagari'; Pattern = '[\u0900-\u097f]' } }
|
||||
'ru' { return @{ Name = 'Cyrillic'; Pattern = '[\u0400-\u04ff]' } }
|
||||
'sa' { return @{ Name = 'Devanagari'; Pattern = '[\u0900-\u097f]' } }
|
||||
'si' { return @{ Name = 'Sinhala'; Pattern = '[\u0d80-\u0dff]' } }
|
||||
'ta' { return @{ Name = 'Tamil'; Pattern = '[\u0b80-\u0bff]' } }
|
||||
'th' { return @{ Name = 'Thai'; Pattern = '[\u0e00-\u0e7f]' } }
|
||||
'ua' { return @{ Name = 'Cyrillic'; Pattern = '[\u0400-\u04ff]' } }
|
||||
'ur' { return @{ Name = 'Arabic'; Pattern = '[\u0600-\u06ff]' } }
|
||||
'zh_CN' { return @{ Name = 'CJK'; Pattern = '[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]' } }
|
||||
'zh_TW' { return @{ Name = 'CJK'; Pattern = '[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]' } }
|
||||
default { return $null }
|
||||
}
|
||||
}
|
||||
|
||||
function Test-TranslationReviewExpectedScript {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LanguageCode,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
$scriptInfo = Get-TranslationReviewScriptInfo -LanguageCode $LanguageCode
|
||||
if ($null -eq $scriptInfo) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
ExpectedScript = $scriptInfo.Name
|
||||
HasExpectedScript = [bool]($Value -match $scriptInfo.Pattern)
|
||||
}
|
||||
}
|
||||
|
||||
function Get-TranslationReviewParityIssues {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourceValue,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TranslatedValue
|
||||
)
|
||||
|
||||
$issues = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
$sourcePlaceholderSignature = (Get-PlaceholderTokens -Value $SourceValue) -join "`n"
|
||||
$translatedPlaceholderSignature = (Get-PlaceholderTokens -Value $TranslatedValue) -join "`n"
|
||||
if ($sourcePlaceholderSignature -ne $translatedPlaceholderSignature) {
|
||||
$issues.Add('Placeholder mismatch')
|
||||
}
|
||||
|
||||
$sourceHtmlSignature = (Get-HtmlTokens -Value $SourceValue) -join "`n"
|
||||
$translatedHtmlSignature = (Get-HtmlTokens -Value $TranslatedValue) -join "`n"
|
||||
if ($sourceHtmlSignature -ne $translatedHtmlSignature) {
|
||||
$issues.Add('HTML fragment mismatch')
|
||||
}
|
||||
|
||||
if ((Get-NewlineCount -Value $SourceValue) -ne (Get-NewlineCount -Value $TranslatedValue)) {
|
||||
$issues.Add('Line-break mismatch')
|
||||
}
|
||||
|
||||
return @($issues)
|
||||
}
|
||||
|
||||
function Format-TranslationReviewMarkdownCell {
|
||||
param(
|
||||
[AllowNull()]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
if ($null -eq $Value) {
|
||||
return '_-_'
|
||||
}
|
||||
|
||||
$escapedValue = $Value -replace '\|', '\|'
|
||||
if ($escapedValue.Length -gt 120) {
|
||||
$escapedValue = $escapedValue.Substring(0, 117) + '...'
|
||||
}
|
||||
|
||||
return $escapedValue
|
||||
}
|
||||
|
||||
if (-not $NeutralJson) {
|
||||
$NeutralJson = Join-Path (Get-TranslationReviewLanguagesDirectory) 'lang_en.json'
|
||||
}
|
||||
|
||||
$languageReference = Get-TranslationReviewLanguageReference
|
||||
if (-not $languageReference.Contains($Language)) {
|
||||
throw "Unknown language code '$Language'."
|
||||
}
|
||||
|
||||
if (-not $ComparisonLanguages) {
|
||||
$ComparisonLanguages = @()
|
||||
foreach ($entry in $languageReference.GetEnumerator()) {
|
||||
$code = [string]$entry.Key
|
||||
if ($code -in @('default', 'en', $Language)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$comparisonPath = Join-Path (Get-TranslationReviewLanguagesDirectory) ("lang_{0}.json" -f $code)
|
||||
if (Test-Path -Path $comparisonPath -PathType Leaf) {
|
||||
$ComparisonLanguages += $code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $OutputPath) {
|
||||
$extension = if ($OutputFormat -eq 'Json') { 'json' } else { 'md' }
|
||||
$OutputPath = Join-Path $repoRoot ("generated\translation-review\review.{0}.{1}" -f $Language, $extension)
|
||||
}
|
||||
|
||||
$neutralMap = Read-OrderedJsonMap -Path $NeutralJson
|
||||
$targetMap = Read-OrderedJsonMap -Path $TargetJson
|
||||
|
||||
$comparisonMaps = [ordered]@{}
|
||||
foreach ($code in $ComparisonLanguages) {
|
||||
$comparisonPath = Join-Path (Get-TranslationReviewLanguagesDirectory) ("lang_{0}.json" -f $code)
|
||||
if (Test-Path -Path $comparisonPath -PathType Leaf) {
|
||||
$comparisonMaps[$code] = Read-OrderedJsonMap -Path $comparisonPath
|
||||
}
|
||||
}
|
||||
|
||||
$parityIssues = [System.Collections.Generic.List[object]]::new()
|
||||
$englishEqualEntries = [System.Collections.Generic.List[object]]::new()
|
||||
$wrongScriptEntries = [System.Collections.Generic.List[object]]::new()
|
||||
$crossLanguageEntries = [System.Collections.Generic.List[object]]::new()
|
||||
|
||||
foreach ($entry in $neutralMap.GetEnumerator()) {
|
||||
$sourceText = [string]$entry.Key
|
||||
$englishValue = [string]$entry.Value
|
||||
|
||||
if (-not $targetMap.Contains($sourceText)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$targetValue = [string]$targetMap[$sourceText]
|
||||
if ([string]::IsNullOrWhiteSpace($targetValue)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$crossLanguage = [ordered]@{}
|
||||
foreach ($code in $comparisonMaps.Keys) {
|
||||
$crossLanguage[$code] = if ($comparisonMaps[$code].Contains($sourceText)) {
|
||||
[string]$comparisonMaps[$code][$sourceText]
|
||||
}
|
||||
else {
|
||||
$null
|
||||
}
|
||||
}
|
||||
|
||||
if ($Language -ne 'en' -and $targetValue -ceq $englishValue -and -not (Test-TranslationReviewTechnicalToken -Value $englishValue)) {
|
||||
$englishEqualEntries.Add([pscustomobject]@{
|
||||
SourceText = $sourceText
|
||||
English = $englishValue
|
||||
Translation = $targetValue
|
||||
CrossLanguage = $crossLanguage
|
||||
})
|
||||
}
|
||||
else {
|
||||
$issues = @(Get-TranslationReviewParityIssues -SourceValue $englishValue -TranslatedValue $targetValue)
|
||||
if ($issues.Count -gt 0) {
|
||||
$parityIssues.Add([pscustomobject]@{
|
||||
SourceText = $sourceText
|
||||
English = $englishValue
|
||||
Translation = $targetValue
|
||||
Issues = $issues
|
||||
CrossLanguage = $crossLanguage
|
||||
})
|
||||
}
|
||||
|
||||
$scriptCheck = Test-TranslationReviewExpectedScript -LanguageCode $Language -Value $targetValue
|
||||
if ($null -ne $scriptCheck -and -not $scriptCheck.HasExpectedScript -and -not (Test-TranslationReviewTechnicalToken -Value $englishValue)) {
|
||||
$wrongScriptEntries.Add([pscustomobject]@{
|
||||
SourceText = $sourceText
|
||||
English = $englishValue
|
||||
Translation = $targetValue
|
||||
ExpectedScript = $scriptCheck.ExpectedScript
|
||||
CrossLanguage = $crossLanguage
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$crossLanguageEntries.Add([pscustomobject]@{
|
||||
SourceText = $sourceText
|
||||
English = $englishValue
|
||||
Translation = $targetValue
|
||||
CrossLanguage = $crossLanguage
|
||||
})
|
||||
}
|
||||
|
||||
$displayName = Get-TranslationReviewDisplayLanguage -LanguageCode $Language -LanguageReference $languageReference
|
||||
|
||||
$output = if ($OutputFormat -eq 'Json') {
|
||||
[pscustomobject]@{
|
||||
Language = $Language
|
||||
DisplayName = $displayName
|
||||
GeneratedAt = (Get-Date -Format 'o')
|
||||
TargetJson = $TargetJson
|
||||
TotalReviewed = $crossLanguageEntries.Count
|
||||
ParityIssueCount = $parityIssues.Count
|
||||
EnglishEqualCount = $englishEqualEntries.Count
|
||||
WrongScriptCount = $wrongScriptEntries.Count
|
||||
ParityIssues = $parityIssues.ToArray()
|
||||
EnglishEqual = $englishEqualEntries.ToArray()
|
||||
WrongScript = $wrongScriptEntries.ToArray()
|
||||
CrossLanguage = if (-not $FlaggedOnly) { $crossLanguageEntries.ToArray() } else { @() }
|
||||
} | ConvertTo-Json -Depth 10
|
||||
}
|
||||
else {
|
||||
$builder = [System.Text.StringBuilder]::new()
|
||||
[void]$builder.AppendLine("# Translation Review: $displayName ($Language)")
|
||||
[void]$builder.AppendLine()
|
||||
[void]$builder.AppendLine("**File**: ``$TargetJson``")
|
||||
[void]$builder.AppendLine("**Generated**: $(Get-Date -Format 'yyyy-MM-dd HH:mm')")
|
||||
[void]$builder.AppendLine("**Comparison languages**: $($comparisonMaps.Keys -join ', ')")
|
||||
[void]$builder.AppendLine()
|
||||
[void]$builder.AppendLine('## Summary')
|
||||
[void]$builder.AppendLine()
|
||||
[void]$builder.AppendLine('| Category | Count |')
|
||||
[void]$builder.AppendLine('|---|---|')
|
||||
[void]$builder.AppendLine("| Reviewed translated entries | $($crossLanguageEntries.Count) |")
|
||||
[void]$builder.AppendLine("| Parity issues | $($parityIssues.Count) |")
|
||||
[void]$builder.AppendLine("| English-equal entries | $($englishEqualEntries.Count) |")
|
||||
[void]$builder.AppendLine("| Wrong script entries | $($wrongScriptEntries.Count) |")
|
||||
[void]$builder.AppendLine()
|
||||
|
||||
[void]$builder.AppendLine('## Parity Issues')
|
||||
[void]$builder.AppendLine()
|
||||
if ($parityIssues.Count -eq 0) {
|
||||
[void]$builder.AppendLine('_No parity issues found._')
|
||||
}
|
||||
else {
|
||||
foreach ($issue in $parityIssues) {
|
||||
[void]$builder.AppendLine("### ``$($issue.SourceText)``")
|
||||
[void]$builder.AppendLine()
|
||||
[void]$builder.AppendLine("- **English**: $($issue.English)")
|
||||
[void]$builder.AppendLine("- **$Language**: $($issue.Translation)")
|
||||
foreach ($problem in $issue.Issues) {
|
||||
[void]$builder.AppendLine("- **Issue**: $problem")
|
||||
}
|
||||
foreach ($code in $issue.CrossLanguage.Keys) {
|
||||
$value = $issue.CrossLanguage[$code]
|
||||
if (-not [string]::IsNullOrWhiteSpace($value) -and $value -ne $issue.English) {
|
||||
[void]$builder.AppendLine("- **$code**: $value")
|
||||
}
|
||||
}
|
||||
[void]$builder.AppendLine()
|
||||
}
|
||||
}
|
||||
[void]$builder.AppendLine()
|
||||
|
||||
[void]$builder.AppendLine('## English-Equal Entries')
|
||||
[void]$builder.AppendLine()
|
||||
if ($englishEqualEntries.Count -eq 0) {
|
||||
[void]$builder.AppendLine('_No suspicious English-equal entries found._')
|
||||
}
|
||||
else {
|
||||
foreach ($item in $englishEqualEntries) {
|
||||
[void]$builder.AppendLine("### ``$($item.SourceText)``")
|
||||
[void]$builder.AppendLine()
|
||||
[void]$builder.AppendLine("- **English**: $($item.English)")
|
||||
[void]$builder.AppendLine("- **$Language**: $($item.Translation)")
|
||||
foreach ($code in $item.CrossLanguage.Keys) {
|
||||
$value = $item.CrossLanguage[$code]
|
||||
if (-not [string]::IsNullOrWhiteSpace($value) -and $value -ne $item.English) {
|
||||
[void]$builder.AppendLine("- **$code**: $value")
|
||||
}
|
||||
}
|
||||
[void]$builder.AppendLine()
|
||||
}
|
||||
}
|
||||
[void]$builder.AppendLine()
|
||||
|
||||
[void]$builder.AppendLine('## Wrong Script')
|
||||
[void]$builder.AppendLine()
|
||||
if ($wrongScriptEntries.Count -eq 0) {
|
||||
$scriptInfo = Get-TranslationReviewScriptInfo -LanguageCode $Language
|
||||
if ($null -eq $scriptInfo) {
|
||||
[void]$builder.AppendLine('_Not applicable for Latin-script languages._')
|
||||
}
|
||||
else {
|
||||
[void]$builder.AppendLine('_No wrong-script entries found._')
|
||||
}
|
||||
}
|
||||
else {
|
||||
foreach ($item in $wrongScriptEntries) {
|
||||
[void]$builder.AppendLine("### ``$($item.SourceText)``")
|
||||
[void]$builder.AppendLine()
|
||||
[void]$builder.AppendLine("- **English**: $($item.English)")
|
||||
[void]$builder.AppendLine("- **$Language**: $($item.Translation)")
|
||||
[void]$builder.AppendLine("- **Expected script**: $($item.ExpectedScript)")
|
||||
foreach ($code in $item.CrossLanguage.Keys) {
|
||||
$value = $item.CrossLanguage[$code]
|
||||
if (-not [string]::IsNullOrWhiteSpace($value) -and $value -ne $item.English) {
|
||||
[void]$builder.AppendLine("- **$code**: $value")
|
||||
}
|
||||
}
|
||||
[void]$builder.AppendLine()
|
||||
}
|
||||
}
|
||||
[void]$builder.AppendLine()
|
||||
|
||||
if (-not $FlaggedOnly) {
|
||||
$comparisonCodes = @($comparisonMaps.Keys)
|
||||
[void]$builder.AppendLine('## Cross-Language Comparison')
|
||||
[void]$builder.AppendLine()
|
||||
[void]$builder.AppendLine('Use this table for spot-checking suspicious wording, wrong language, and unusual differences against related languages.')
|
||||
[void]$builder.AppendLine()
|
||||
$header = '| Source | English | ' + $Language + ' | ' + ($comparisonCodes -join ' | ') + ' |'
|
||||
$divider = '|---|---|' + ('---|' * ($comparisonCodes.Count + 1))
|
||||
[void]$builder.AppendLine($header)
|
||||
[void]$builder.AppendLine($divider)
|
||||
foreach ($row in $crossLanguageEntries) {
|
||||
$cells = New-Object System.Collections.Generic.List[string]
|
||||
$cells.Add((Format-TranslationReviewMarkdownCell -Value $row.SourceText))
|
||||
$cells.Add((Format-TranslationReviewMarkdownCell -Value $row.English))
|
||||
$cells.Add((Format-TranslationReviewMarkdownCell -Value $row.Translation))
|
||||
foreach ($code in $comparisonCodes) {
|
||||
$cells.Add((Format-TranslationReviewMarkdownCell -Value $row.CrossLanguage[$code]))
|
||||
}
|
||||
|
||||
[void]$builder.AppendLine('| ' + ($cells -join ' | ') + ' |')
|
||||
}
|
||||
}
|
||||
|
||||
$builder.ToString()
|
||||
}
|
||||
|
||||
$outputDirectory = Split-Path -Path $OutputPath -Parent
|
||||
if (-not (Test-Path -Path $outputDirectory -PathType Container)) {
|
||||
New-Item -Path $outputDirectory -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
[System.IO.File]::WriteAllText($OutputPath, [string]$output, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
Write-Host "Review written to: $OutputPath"
|
||||
Write-Host " Parity issues: $($parityIssues.Count)"
|
||||
Write-Host " English-equal: $($englishEqualEntries.Count)"
|
||||
Write-Host " Wrong script: $($wrongScriptEntries.Count)"
|
||||
Write-Host " Entries reviewed: $($crossLanguageEntries.Count)"
|
||||
|
||||
[pscustomobject]@{
|
||||
OutputPath = $OutputPath
|
||||
ParityIssueCount = $parityIssues.Count
|
||||
EnglishEqualCount = $englishEqualEntries.Count
|
||||
WrongScriptCount = $wrongScriptEntries.Count
|
||||
ReviewedEntryCount = $crossLanguageEntries.Count
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: translation-source-sync
|
||||
description: Synchronizes the UniGetUI English language file with source-code usage, identifies missing translation keys, removes unused entries, reports localization drift, and can reorder locale files to match the English key ordering. Use when the user asks to sync language files, missing translations, i18n drift, or align locale file ordering with English.
|
||||
---
|
||||
|
||||
# translation source sync
|
||||
|
||||
Use this skill when UniGetUI source code changed and you need to keep [src/Languages/lang_en.json](src/Languages/lang_en.json) aligned with the strings actually used by the application, or when you need downstream locale files reordered to match the English key ordering.
|
||||
|
||||
It scans supported C#, WinUI XAML, and Avalonia AXAML patterns, finds missing English keys, removes unused entries, reports translation-source warnings that still need manual cleanup, and can align locale file ordering to the English layout.
|
||||
|
||||
## Scope
|
||||
|
||||
- Extract literal translation keys from supported UniGetUI source patterns.
|
||||
- Add new English keys missing from `lang_en.json`.
|
||||
- Remove English keys that are no longer used.
|
||||
- Reorder non-English locale files to match the English key ordering.
|
||||
- Warn about interpolated `CoreTools.Translate($"...")` calls that are not synchronized automatically.
|
||||
- Leave downstream language propagation to the existing translation diff workflow.
|
||||
|
||||
## Supported extraction sources
|
||||
|
||||
- `CoreTools.Translate("...")` in C#.
|
||||
- `CoreTools.AutoTranslated("...")` in C#.
|
||||
- `widgets:TranslatedTextBlock Text="..."` in WinUI XAML.
|
||||
- `settings:TranslatedTextBlock Text="..."` in Avalonia AXAML.
|
||||
- `{t:Translate Some text}` and `{t:Translate Text='A, B, C'}` in Avalonia AXAML.
|
||||
|
||||
## Out of scope for v1
|
||||
|
||||
- Interpolated translation calls such as `CoreTools.Translate($"Running {value}")`.
|
||||
- Non-literal translation inputs passed through variables.
|
||||
- CI enforcement.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/sync-translation-sources.ps1`: Skill wrapper around the repository sync script.
|
||||
- `scripts/set-translation-boundary-order.ps1`: Skill wrapper around the repository locale reordering script.
|
||||
- `scripts/test-translation-source-sync.ps1`: Skill wrapper around the repository smoke test.
|
||||
- `../../../../scripts/translation/Sync-TranslationSources.ps1`: Canonical repository implementation used by the wrapper.
|
||||
- `../../../../scripts/translation/Set-TranslationBoundaryOrder.ps1`: Canonical repository implementation used by the wrapper.
|
||||
- `../../../../scripts/translation/Test-TranslationSourceSync.ps1`: Canonical smoke test used by the wrapper.
|
||||
|
||||
## Usage
|
||||
|
||||
Synchronize the checked-in English language file in place:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-source-sync/scripts/sync-translation-sources.ps1
|
||||
```
|
||||
|
||||
Check whether the English file is out of sync without writing changes:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-source-sync/scripts/sync-translation-sources.ps1 -CheckOnly
|
||||
```
|
||||
|
||||
Reorder locale files to match the English key ordering:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-source-sync/scripts/set-translation-boundary-order.ps1
|
||||
```
|
||||
|
||||
Preview whether locale files need reordering without writing changes:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-source-sync/scripts/set-translation-boundary-order.ps1 -CheckOnly
|
||||
```
|
||||
|
||||
Run the smoke test:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-source-sync/scripts/test-translation-source-sync.ps1
|
||||
```
|
||||
|
||||
## Recommended workflow
|
||||
|
||||
1. Run translation source sync after changing any translatable source string.
|
||||
2. If `-CheckOnly` reports drift, run the full sync to rewrite `lang_en.json`.
|
||||
3. Review warnings for interpolated translation calls and convert them to stable literals or add the missing English keys manually when needed.
|
||||
4. Run `pwsh ./.agents/skills/translation-source-sync/scripts/set-translation-boundary-order.ps1` to align locale ordering with English.
|
||||
5. Run `pwsh ./scripts/translation/Verify-Translations.ps1` to confirm the language files still validate cleanly.
|
||||
6. Run `pwsh ./.agents/skills/translation-source-sync/scripts/test-translation-source-sync.ps1` if you changed the sync workflow itself.
|
||||
7. Export changed work for translators with [translation-diff-export](../translation-diff-export/SKILL.md).
|
||||
|
||||
## Notes
|
||||
|
||||
- Existing English values are preserved for retained keys; newly added English entries default to `key == value`.
|
||||
- The sync script preserves current key order for retained entries and appends newly discovered keys deterministically.
|
||||
- The locale reorder script follows English as the canonical order and appends unmapped locale-only keys at the end.
|
||||
- The smoke test uses a temporary synthetic repo so it does not mutate the checked-in language files.
|
||||
@@ -0,0 +1,26 @@
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[string]$RepositoryRoot,
|
||||
[string]$EnglishFilePath,
|
||||
[string]$LanguagesDirectory,
|
||||
[string]$InventoryOutputPath,
|
||||
[switch]$IncludeEnglish,
|
||||
[switch]$CheckOnly
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..\..'))
|
||||
$scriptPath = Join-Path $repoRoot 'scripts\translation\Set-TranslationBoundaryOrder.ps1'
|
||||
if (-not (Test-Path -Path $scriptPath -PathType Leaf)) {
|
||||
throw "Translation boundary reorder script not found: $scriptPath"
|
||||
}
|
||||
|
||||
& $scriptPath `
|
||||
-RepositoryRoot $RepositoryRoot `
|
||||
-EnglishFilePath $EnglishFilePath `
|
||||
-LanguagesDirectory $LanguagesDirectory `
|
||||
-InventoryOutputPath $InventoryOutputPath `
|
||||
-IncludeEnglish:$IncludeEnglish.IsPresent `
|
||||
-CheckOnly:$CheckOnly.IsPresent
|
||||
@@ -0,0 +1,17 @@
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[string]$RepositoryRoot,
|
||||
[string]$EnglishFilePath,
|
||||
[switch]$CheckOnly
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..\..'))
|
||||
$scriptPath = Join-Path $repoRoot 'scripts\translation\Sync-TranslationSources.ps1'
|
||||
if (-not (Test-Path -Path $scriptPath -PathType Leaf)) {
|
||||
throw "Translation source sync script not found: $scriptPath"
|
||||
}
|
||||
|
||||
& $scriptPath -RepositoryRoot $RepositoryRoot -EnglishFilePath $EnglishFilePath -CheckOnly:$CheckOnly.IsPresent
|
||||
@@ -0,0 +1,10 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..\..'))
|
||||
$scriptPath = Join-Path $repoRoot 'scripts\translation\Test-TranslationSourceSync.ps1'
|
||||
if (-not (Test-Path -Path $scriptPath -PathType Leaf)) {
|
||||
throw "Translation source sync smoke test script not found: $scriptPath"
|
||||
}
|
||||
|
||||
& $scriptPath
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: translation-status
|
||||
description: Analyzes checked-in UniGetUI language files to calculate translation percentages, surface untranslated coverage data, and generate localization status reports.
|
||||
---
|
||||
|
||||
# translation status
|
||||
|
||||
Use this skill when the user asks for UniGetUI translation progress, localization or i18n coverage, untranslated-language reports, or a language support status summary.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- PowerShell 7 (`pwsh`).
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/get-translation-status.ps1`: Skill wrapper you should invoke from the skill.
|
||||
- `../../../../scripts/translation/Get-TranslationStatus.ps1`: Canonical repository implementation delegated to by the wrapper.
|
||||
|
||||
## Usage
|
||||
|
||||
Show the default table summary:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-status/scripts/get-translation-status.ps1
|
||||
```
|
||||
|
||||
Show only incomplete languages as markdown:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-status/scripts/get-translation-status.ps1 \
|
||||
-OutputFormat Markdown \
|
||||
-OnlyIncomplete
|
||||
```
|
||||
|
||||
Write JSON output to a file:
|
||||
|
||||
```powershell
|
||||
pwsh ./.agents/skills/translation-status/scripts/get-translation-status.ps1 \
|
||||
-OutputFormat Json \
|
||||
-OutputPath ./generated/translation-status.json
|
||||
```
|
||||
|
||||
## Output Fields
|
||||
|
||||
- `Code`: language code
|
||||
- `Language`: display name
|
||||
- `Completion`: computed completion percentage using English active keys only
|
||||
- `Translated`, `Missing`, `Empty`, `SourceEqual`, `Extra`: per-language entry counts
|
||||
- `Stored` and `Delta`: stored percentage metadata and difference from the computed result
|
||||
|
||||
## Notes
|
||||
|
||||
- The script treats source-equal values as untranslated for non-English languages.
|
||||
- Completion never reports `100%` unless every active English key is present and translated.
|
||||
- `Extra` counts locale-only keys not present in the English file.
|
||||
- Use `-IncludeEnglish` if you want the `en` row included in the report.
|
||||
- Use `-OnlyIncomplete` to focus on languages that still need work.
|
||||
- For the full parameter surface, inspect `../../../../scripts/translation/Get-TranslationStatus.ps1`.
|
||||
@@ -0,0 +1,22 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Table', 'Json', 'Markdown')]
|
||||
[string]$OutputFormat = 'Table',
|
||||
|
||||
[switch]$IncludeEnglish,
|
||||
|
||||
[switch]$OnlyIncomplete,
|
||||
|
||||
[string]$OutputPath
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..\..'))
|
||||
$scriptPath = Join-Path $repoRoot 'scripts\translation\Get-TranslationStatus.ps1'
|
||||
if (-not (Test-Path -Path $scriptPath -PathType Leaf)) {
|
||||
throw "Translation status script not found: $scriptPath"
|
||||
}
|
||||
|
||||
& $scriptPath -OutputFormat $OutputFormat -IncludeEnglish:$IncludeEnglish.IsPresent -OnlyIncomplete:$OnlyIncomplete.IsPresent -OutputPath $OutputPath
|
||||
@@ -0,0 +1 @@
|
||||
../.agents/skills
|
||||
@@ -0,0 +1,65 @@
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "python"
|
||||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
runtime_version = "3.x.x"
|
||||
|
||||
[analyzers.pylint]
|
||||
enabled = true
|
||||
config = """
|
||||
[MESSAGES CONTROL]
|
||||
disable = C0111 # Disables missing docstring warnings
|
||||
"""
|
||||
|
||||
[[analyzers]]
|
||||
name = "javascript"
|
||||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
plugins = [
|
||||
"react",
|
||||
"vue",
|
||||
"angular"
|
||||
]
|
||||
environment = [
|
||||
"nodejs",
|
||||
"mocha",
|
||||
"mongo",
|
||||
"browser",
|
||||
"jasmine",
|
||||
"cypress",
|
||||
"vitest",
|
||||
"jquery",
|
||||
"jest"
|
||||
]
|
||||
|
||||
[[transformers]]
|
||||
name = "autopep8"
|
||||
enabled = false # Disables autopep8 transformer
|
||||
|
||||
[[transformers]]
|
||||
name = "isort"
|
||||
enabled = false # Disables isort transformer
|
||||
|
||||
[[transformers]]
|
||||
name = "black"
|
||||
enabled = false # Disables black transformer
|
||||
|
||||
[[transformers]]
|
||||
name = "yapf"
|
||||
enabled = false # Disables yapf transformer
|
||||
|
||||
[[transformers]]
|
||||
name = "ruff"
|
||||
enabled = false # Disables ruff transformer
|
||||
|
||||
[[transformers]]
|
||||
name = "standardjs"
|
||||
enabled = false # Disables standardjs transformer
|
||||
|
||||
[[transformers]]
|
||||
name = "prettier"
|
||||
enabled = false # Disables prettier transformer
|
||||
@@ -0,0 +1,2 @@
|
||||
src/UniGetUI.PackageEngine.Managers.Chocolatey/choco-cli/** linguist-vendored
|
||||
.githooks/* text eol=lf
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel)
|
||||
cd "$repo_root"
|
||||
|
||||
staged_files=$(git diff --cached --name-only --diff-filter=ACMR -- src | grep -E '\.(cs|csproj|props|targets|editorconfig|json|sln|slnx)$' || true)
|
||||
|
||||
if [ -z "$staged_files" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
unstaged_files=$(git diff --name-only -- $staged_files || true)
|
||||
if [ -n "$unstaged_files" ]; then
|
||||
echo "pre-commit: relevant files have unstaged changes."
|
||||
echo "Stage or stash them before committing so dotnet format does not rewrite mixed content."
|
||||
printf '%s\n' "$unstaged_files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v dotnet >/dev/null 2>&1; then
|
||||
echo "pre-commit: dotnet CLI not found; skipping whitespace formatting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "pre-commit: running dotnet format whitespace on staged src files"
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
dotnet format whitespace src --folder --verbosity minimal --include $staged_files
|
||||
|
||||
if ! git diff --quiet -- $staged_files; then
|
||||
git add -- $staged_files
|
||||
echo "pre-commit: formatting updates were staged. Review them and run git commit again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,3 @@
|
||||
# File auto-generated and managed by Devops
|
||||
/.github/ @devolutions/devops @devolutions/architecture-maintainers @GabrielDuf # Gabriel Dufresne
|
||||
/.github/dependabot.yml @devolutions/security-managers
|
||||
@@ -0,0 +1,68 @@
|
||||
name: '🐞 Report a bug or an issue'
|
||||
description: Report issues or unexpected behaviors.
|
||||
title: "[BUG] (Enter your description here)"
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please confirm these before moving forward
|
||||
description: Please confirm the following before posting your issue.
|
||||
options:
|
||||
- label: I have searched for my issue and have not found a work-in-progress/duplicate/resolved issue.
|
||||
required: true
|
||||
- label: I have tested that this issue has not been fixed in the latest [(beta or stable) release](https://github.com/Devolutions/UniGetUI/releases/).
|
||||
required: true
|
||||
- label: I have checked the [FAQ](https://github.com/Devolutions/UniGetUI#frequently-asked-questions) section for solutions.
|
||||
required: true
|
||||
- label: This issue is about a bug (if it is not, please use the [correct template](https://github.com/Devolutions/UniGetUI/issues/new/choose)).
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: UniGetUI Version
|
||||
placeholder: 'x.y.z (e.g., 3.1.0)'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Windows version, edition, and architecture
|
||||
placeholder: Windows 11 Pro 10.0.22000.0 x64
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe your issue
|
||||
placeholder: Explain the issue you are experiencing, providing as many details as possible.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce the issue
|
||||
placeholder: How can this issue be reproduced? List here, if known, the steps followed before this issue appeared.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: UniGetUI Log
|
||||
render: "text"
|
||||
placeholder: Paste your UniGetUI logs here. Click the button on the bottom-left corner of UniGetUI -> UniGetUI Log.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Package Managers Logs
|
||||
render: "text"
|
||||
placeholder: Paste your Package Manager logs here. Click the button on the bottom-left corner of UniGetUI -> Package Manager logs.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Relevant information
|
||||
placeholder: Other relevant information about this issue. Perhaps you have some special settings enabled or any other detail that could be related.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots and videos
|
||||
placeholder: If applicable, please post here a video or a screenshot of the issue.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,12 @@
|
||||
blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: 📦 ISSUES INSTALLING/UPDATING A PACKAGE
|
||||
url: https://github.com/Devolutions/UniGetUI
|
||||
about: PLEASE READ THIS BEFORE CREATING AN ISSUE RELATED TO A SPECIFIC PACKAGE
|
||||
- name: 🔒 Security issue or vulnerability
|
||||
url: https://devolutions.net/security/
|
||||
about: Found a security issue? Please report it via the Devolutions security page
|
||||
- name: 📧 Contact us directly, in private
|
||||
url: https://devolutions.net/contact/
|
||||
about: Please use only for private inquiries that should not be posted publicly
|
||||
@@ -0,0 +1,28 @@
|
||||
name: '🚧 Suggest an improvement for an existing feature'
|
||||
description: Propose an improvement to UniGetUI.
|
||||
title: "[IMPROVEMENT] (Enter your description here)"
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please confirm these before moving forward.
|
||||
description: Please confirm the following before posting your issue.
|
||||
options:
|
||||
- label: I have searched for my feature proposal and have not found a work-in-progress/duplicate/resolved/discarded issue.
|
||||
required: true
|
||||
- label: This improvement refers to an existing feature. If you want to suggest a new feature, please use [this template](https://github.com/Devolutions/UniGetUI/issues/new?labels=new-feature&projects=&template=feature-request.yml&title=%5BFEATURE+REQUEST%5D+%28Enter+your+description+here%29).
|
||||
required: true
|
||||
- label: This improvement is not a bug. If you want to report a bug, please use [this template](https://github.com/Devolutions/UniGetUI/issues/new?labels=bug&projects=&template=bug-issue.yml&title=%5BBUG%5D+%28Enter+your+description+here%29).
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the improvement
|
||||
placeholder: Explain how you see this enhancement, providing as many details as possible.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe how this improvement could help users
|
||||
placeholder: For what would it be useful?
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,26 @@
|
||||
name: '💡 Propose a new feature'
|
||||
description: Propose a new feature that could be useful in UniGetUI.
|
||||
title: "[FEATURE REQUEST] (Enter your description here)"
|
||||
labels: ["new-feature"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please confirm these before moving forward.
|
||||
description: Please confirm the following before posting your issue.
|
||||
options:
|
||||
- label: I have searched for my feature proposal and have not found a work-in-progress/duplicate/resolved/discarded issue.
|
||||
required: true
|
||||
- label: This proposal is a completely new feature. If you want to suggest an improvement or an enhancement, please use [this template](https://github.com/Devolutions/UniGetUI/issues/new?labels=enhancement&projects=&template=enhancement-improvement.yml&title=%5BENHANCEMENT%5D+%28Enter+your+description+here%29).
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the new feature
|
||||
placeholder: Explain how you see this new feature, providing as many details as possible.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe how this new feature could help users
|
||||
placeholder: For what would it be useful?
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,35 @@
|
||||
name: '🚑 Report a crash or a hang'
|
||||
description: UniGetUI is not launching, is hard-crashing, or is hanging at some point.
|
||||
title: "[CRASH] (Enter your description here)"
|
||||
labels: ["bug", "important"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please confirm these before moving forward.
|
||||
description: Please confirm the following before posting your issue.
|
||||
options:
|
||||
- label: I have tried deleting a folder named `UniGetUI` under `%UserProfile%\AppData\Local\UniGetUI`.
|
||||
required: true
|
||||
- label: I have tried reinstalling UniGetUI.
|
||||
required: true
|
||||
- label: I have tested that this issue has not been fixed in the latest [(beta or stable) release](https://github.com/Devolutions/UniGetUI/releases/).
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe your crash
|
||||
placeholder: What were you doing when this happened?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Logs (if possible)
|
||||
placeholder: If you get an error report or a message, please post it here.
|
||||
render: "text"
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: More details
|
||||
placeholder: Do you have any other valuable information about this issue?
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,74 @@
|
||||
name: '🧩 Report an issue with Widgets for UniGetUI'
|
||||
description: Report issues or unexpected behaviors with Widgets for UniGetUI.
|
||||
title: "[WIDGETS BUG] (Enter your description here)"
|
||||
labels: ["bug", "widgets-for-unigetui"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please confirm these before moving forward
|
||||
description: Please confirm the following before posting your issue.
|
||||
options:
|
||||
- label: I have searched for my issue and have not found a work-in-progress/duplicate/resolved issue.
|
||||
required: true
|
||||
- label: I have tested that this issue has not been fixed in the latest [(beta or stable) release](https://github.com/Devolutions/UniGetUI/releases/).
|
||||
required: true
|
||||
- label: I have checked the [FAQ](https://github.com/Devolutions/UniGetUI#frequently-asked-questions) section for solutions.
|
||||
required: true
|
||||
- label: This issue is about a bug (if it is not, please use the [correct template](https://github.com/Devolutions/UniGetUI/issues/new/choose)).
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: UniGetUI Version
|
||||
placeholder: 'x.y.z (e.g., 3.1.0)'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Widgets for UniGetUI Version
|
||||
placeholder: 'x.y.z.w (e.g., 0.6.1.0)'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Windows version, edition, and architecture
|
||||
placeholder: Windows 11 Pro 10.0.22000.0 x64
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe your issue
|
||||
placeholder: Explain the issue you are experiencing, providing as many details as possible.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce the issue
|
||||
placeholder: How can this issue be reproduced? List here, if known, the steps followed before this issue appeared.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: UniGetUI Log
|
||||
render: "text"
|
||||
placeholder: Paste your UniGetUI logs here. Click the button on the bottom-left corner of UniGetUI -> UniGetUI Log.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Package Managers Logs
|
||||
render: "text"
|
||||
placeholder: Paste your Package Manager logs here. Click the button on the bottom-left corner of UniGetUI -> Package Manager logs.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Relevant information
|
||||
placeholder: Other relevant information about this issue. Perhaps you have some special settings enabled or any other detail that could be related.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots and videos
|
||||
placeholder: If applicable, please post a video or a screenshot of the issue here.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,14 @@
|
||||
<!-- Provide a general summary of your changes in the title above -->
|
||||
- [ ] **I have read the [contributing guidelines](https://github.com/Devolutions/UniGetUI/blob/main/CONTRIBUTING.md#coding), and I agree with the [Code of Conduct](https://github.com/Devolutions/UniGetUI/blob/main/CODE_OF_CONDUCT.md)**.
|
||||
- [ ] **Have you checked that there aren't other open [pull requests](https://github.com/Devolutions/UniGetUI/pulls) for the same changes?**
|
||||
- [ ] **Have you tested that the committed code can be executed without errors?**
|
||||
- [ ] **Have you confirmed that this issue is caused by UniGetUI itself, and not by the package manager or the package involved?**
|
||||
If the same issue can be reproduced outside UniGetUI with the relevant package manager or with the package itself, please report it there first. UniGetUI should only be used to track issues that are specific to UniGetUI's behavior or integration.
|
||||
-----
|
||||
|
||||
<!-- optionally, explain here about the committed code -->
|
||||
-----
|
||||
<!-- insert below the issue number (if applicable, do not create a new issue to justify a PR) -->
|
||||
Closes #XXXX
|
||||
<!-- and/or -->
|
||||
Relates to #XXXX
|
||||
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
actions-deps:
|
||||
patterns:
|
||||
- "*"
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"enabledManagers": [
|
||||
"nuget"
|
||||
],
|
||||
"dependencyDashboard": true
|
||||
}
|
||||
@@ -0,0 +1,896 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version (required, e.g. 2026.1.0)'
|
||||
required: true
|
||||
draft-release:
|
||||
description: 'Create the GitHub Release as a draft'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
skip-publish:
|
||||
description: 'Skip publishing to GitHub Releases'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
dry-run:
|
||||
description: 'Dry run (simulate without publishing)'
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
name: Preflight
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
package-env: ${{ steps.info.outputs.package-env }}
|
||||
package-version: ${{ steps.info.outputs.package-version }}
|
||||
onedrive-version: ${{ steps.info.outputs.onedrive-version }}
|
||||
draft-release: ${{ steps.info.outputs.draft-release }}
|
||||
skip-publish: ${{ steps.info.outputs.skip-publish }}
|
||||
dry-run: ${{ steps.info.outputs.dry-run }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Resolve build parameters
|
||||
id: info
|
||||
shell: pwsh
|
||||
run: |
|
||||
$IsProductionBranch = @('main', 'master') -contains '${{ github.ref_name }}'
|
||||
|
||||
try { $DraftRelease = [System.Boolean]::Parse('${{ inputs.draft-release }}') } catch { $DraftRelease = $false }
|
||||
try { $SkipPublish = [System.Boolean]::Parse('${{ inputs.skip-publish }}') } catch { $SkipPublish = $false }
|
||||
try { $DryRun = [System.Boolean]::Parse('${{ inputs.dry-run }}') } catch { $DryRun = $true }
|
||||
|
||||
$PackageEnv = if ($IsProductionBranch) {
|
||||
"publish-prod"
|
||||
} else {
|
||||
"publish-test"
|
||||
}
|
||||
|
||||
if (-Not $IsProductionBranch) {
|
||||
$DryRun = $true # force dry run when not on main/master branch
|
||||
}
|
||||
|
||||
if (-Not $SkipPublish -And $PackageEnv -ne 'publish-prod') {
|
||||
$DryRun = $true # force dry run when publishing outside production environment
|
||||
}
|
||||
|
||||
$PackageVersion = '${{ inputs.version }}'
|
||||
if ([string]::IsNullOrWhiteSpace($PackageVersion)) {
|
||||
throw "The workflow_dispatch version input is required."
|
||||
}
|
||||
|
||||
echo "package-env=$PackageEnv" >> $Env:GITHUB_OUTPUT
|
||||
$OneDriveVersion = "$PackageVersion.0"
|
||||
|
||||
echo "package-version=$PackageVersion" >> $Env:GITHUB_OUTPUT
|
||||
echo "onedrive-version=$OneDriveVersion" >> $Env:GITHUB_OUTPUT
|
||||
echo "draft-release=$($DraftRelease.ToString().ToLower())" >> $Env:GITHUB_OUTPUT
|
||||
echo "skip-publish=$($SkipPublish.ToString().ToLower())" >> $Env:GITHUB_OUTPUT
|
||||
echo "dry-run=$($DryRun.ToString().ToLower())" >> $Env:GITHUB_OUTPUT
|
||||
|
||||
echo "::notice::Environment: $PackageEnv"
|
||||
echo "::notice::Version: $PackageVersion"
|
||||
echo "::notice::DraftRelease: $DraftRelease"
|
||||
echo "::notice::DryRun: $DryRun"
|
||||
|
||||
build:
|
||||
name: Build & Sign (${{ matrix.platform }})
|
||||
runs-on: windows-latest
|
||||
needs: [preflight]
|
||||
environment: ${{ needs.preflight.outputs.package-env }}
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
UNIGETUI_GITHUB_CLIENT_ID: ${{ secrets.UNIGETUI_GITHUB_CLIENT_ID }}
|
||||
UNIGETUI_GITHUB_CLIENT_SECRET: ${{ secrets.UNIGETUI_GITHUB_CLIENT_SECRET }}
|
||||
UNIGETUI_OPENSEARCH_USERNAME: ${{ secrets.UNIGETUI_OPENSEARCH_USERNAME }}
|
||||
UNIGETUI_OPENSEARCH_PASSWORD: ${{ secrets.UNIGETUI_OPENSEARCH_PASSWORD }}
|
||||
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [x64, arm64]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Validate GitHub OAuth secrets
|
||||
shell: pwsh
|
||||
run: |
|
||||
if ([string]::IsNullOrWhiteSpace($env:UNIGETUI_GITHUB_CLIENT_ID)) {
|
||||
throw "UNIGETUI_GITHUB_CLIENT_ID is not configured for this build environment."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($env:UNIGETUI_GITHUB_CLIENT_SECRET)) {
|
||||
throw "UNIGETUI_GITHUB_CLIENT_SECRET is not configured for this build environment."
|
||||
}
|
||||
|
||||
Write-Host "::notice::GitHub OAuth secrets are configured for this build."
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ env.NUGET_PACKAGES }}
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('global.json', 'src/**/*.csproj', 'src/**/*.props', 'src/**/*.targets', 'src/**/*.sln', 'src/**/*.slnx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install Inno Setup
|
||||
shell: pwsh
|
||||
run: |
|
||||
choco install innosetup -y --no-progress
|
||||
echo "C:\Program Files (x86)\Inno Setup 6" >> $Env:GITHUB_PATH
|
||||
|
||||
- name: Install code-signing tools
|
||||
shell: pwsh
|
||||
run: |
|
||||
dotnet tool install --global AzureSignTool
|
||||
Install-Module -Name Devolutions.Authenticode -Force
|
||||
|
||||
# Trust test code-signing CA
|
||||
$TestCertsUrl = "https://raw.githubusercontent.com/Devolutions/devolutions-authenticode/master/data/certs"
|
||||
Invoke-WebRequest -Uri "$TestCertsUrl/authenticode-test-ca.crt" -OutFile ".\authenticode-test-ca.crt"
|
||||
Import-Certificate -FilePath ".\authenticode-test-ca.crt" -CertStoreLocation "cert:\LocalMachine\Root"
|
||||
Remove-Item ".\authenticode-test-ca.crt" -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
- name: Set version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
|
||||
.\scripts\set-version.ps1 -Version $PackageVersion
|
||||
|
||||
- name: Restore dependencies
|
||||
working-directory: src
|
||||
run: dotnet restore UniGetUI.Windows.slnx
|
||||
|
||||
- name: Run tests
|
||||
working-directory: src
|
||||
shell: pwsh
|
||||
run: |
|
||||
dotnet build-server shutdown
|
||||
# Retry once to handle flaky tests (e.g. TaskRecyclerTests uses Random)
|
||||
dotnet test UniGetUI.Windows.slnx --no-restore --verbosity q --nologo /p:Platform=x64 /p:UseSharedCompilation=false /m:1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "::warning::First test run failed, retrying..."
|
||||
dotnet build-server shutdown
|
||||
dotnet test UniGetUI.Windows.slnx --no-restore --verbosity q --nologo /p:Platform=x64 /p:UseSharedCompilation=false /m:1
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
}
|
||||
|
||||
- name: Publish
|
||||
shell: pwsh
|
||||
run: |
|
||||
$Platform = '${{ matrix.platform }}'
|
||||
$TargetFramework = (
|
||||
dotnet msbuild src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj `
|
||||
/nologo `
|
||||
-getProperty:TargetFramework `
|
||||
/p:Configuration=Release `
|
||||
/p:Platform=$Platform `
|
||||
/p:RuntimeIdentifier=win-$Platform |
|
||||
Select-Object -Last 1
|
||||
).Trim()
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($TargetFramework)) {
|
||||
throw "Could not resolve the Avalonia target framework from MSBuild"
|
||||
}
|
||||
|
||||
dotnet publish src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj `
|
||||
/noLogo `
|
||||
/p:Configuration=Release `
|
||||
/p:Platform=$Platform `
|
||||
/p:RuntimeIdentifier=win-$Platform `
|
||||
/p:PublishProfile=Win-$Platform-NativeAot `
|
||||
/p:UseSharedCompilation=false `
|
||||
/p:BuildInParallel=false `
|
||||
/m:1 `
|
||||
-v m
|
||||
if ($LASTEXITCODE -ne 0) { throw "dotnet publish Avalonia failed" }
|
||||
|
||||
# Stage binaries
|
||||
$PublishDir = "src/UniGetUI.Avalonia/bin/$Platform/Release/$TargetFramework/win-$Platform/publish"
|
||||
if (Test-Path "unigetui_bin") { Remove-Item "unigetui_bin" -Recurse -Force }
|
||||
New-Item "unigetui_bin" -ItemType Directory | Out-Null
|
||||
Get-ChildItem $PublishDir | Move-Item -Destination "unigetui_bin" -Force
|
||||
|
||||
if (-not (Test-Path "unigetui_bin/UniGetUI.exe")) {
|
||||
throw "Windows app host was not produced at unigetui_bin/UniGetUI.exe"
|
||||
}
|
||||
|
||||
$MaxShippedPdbSizeBytes = 1MB
|
||||
$PdbsToRemove = Get-ChildItem "unigetui_bin" -Filter "*.pdb" -File -Recurse | Where-Object {
|
||||
$_.Length -gt $MaxShippedPdbSizeBytes
|
||||
}
|
||||
|
||||
if ($PdbsToRemove.Count -gt 0) {
|
||||
$RemovedPdbBytes = ($PdbsToRemove | Measure-Object -Property Length -Sum).Sum
|
||||
$PdbsToRemove | Remove-Item -Force
|
||||
Write-Host ("Removed {0} oversized PDBs above {1:N2} MiB ({2:N2} MiB total)." -f $PdbsToRemove.Count, ($MaxShippedPdbSizeBytes / 1MB), ($RemovedPdbBytes / 1MB))
|
||||
}
|
||||
|
||||
- name: Code-sign binaries
|
||||
if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ListPath = Join-Path $PWD "signing-files.txt"
|
||||
$files = Get-ChildItem "unigetui_bin" -Recurse -Include "*.exe", "*.dll" | Where-Object {
|
||||
(Get-AuthenticodeSignature $_.FullName).Status -eq "NotSigned"
|
||||
}
|
||||
$files.FullName | Set-Content $ListPath
|
||||
Write-Host "Signing list contains $($files.Count) files."
|
||||
|
||||
.\scripts\sign.ps1 `
|
||||
-FileListPath $ListPath `
|
||||
-AzureTenantId '${{ secrets.AZURE_TENANT_ID }}' `
|
||||
-KeyVaultUrl '${{ secrets.CODE_SIGNING_KEYVAULT_URL }}' `
|
||||
-ClientId '${{ secrets.CODE_SIGNING_CLIENT_ID }}' `
|
||||
-ClientSecret '${{ secrets.CODE_SIGNING_CLIENT_SECRET }}' `
|
||||
-CertificateName '${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}' `
|
||||
-TimestampServer '${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}'
|
||||
|
||||
- name: Build installer
|
||||
shell: pwsh
|
||||
run: |
|
||||
$Platform = '${{ matrix.platform }}'
|
||||
$OutputDir = Join-Path $PWD "output"
|
||||
New-Item $OutputDir -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
.\scripts\refresh-integrity-tree.ps1 -Path $PWD/unigetui_bin -FailOnUnexpectedFiles
|
||||
|
||||
# Configure Inno Setup to use AzureSignTool
|
||||
$IssPath = "UniGetUI.iss"
|
||||
|
||||
# Build the installer (signing of the installer itself happens in the next step)
|
||||
# Temporarily remove SignTool line so ISCC doesn't try to sign during build
|
||||
$issContent = Get-Content $IssPath -Raw
|
||||
|
||||
try {
|
||||
$issContentNoSign = $issContent -Replace '(?m)^SignTool=.*$', '; SignTool=azsign (disabled for CI, signed separately)'
|
||||
$issContentNoSign = $issContentNoSign -Replace '(?m)^SignedUninstaller=yes', 'SignedUninstaller=no'
|
||||
Set-Content $IssPath $issContentNoSign -NoNewline
|
||||
|
||||
$InstallerBaseName = "UniGetUI.Installer.$Platform"
|
||||
$IsccArgs = @($IssPath, "/F$InstallerBaseName", "/O$OutputDir")
|
||||
if ([System.Boolean]::Parse('${{ needs.preflight.outputs.dry-run }}') -eq $false) {
|
||||
Write-Host "Using lzma/ultra64 installer compression for release build."
|
||||
$IsccArgs = @('/DInstallerCompression=lzma/ultra64') + $IsccArgs
|
||||
}
|
||||
|
||||
& ISCC.exe @IsccArgs
|
||||
if ($LASTEXITCODE -ne 0) { throw "Inno Setup failed with exit code $LASTEXITCODE" }
|
||||
}
|
||||
finally {
|
||||
Set-Content $IssPath $issContent -NoNewline
|
||||
}
|
||||
|
||||
- name: Stage output
|
||||
shell: pwsh
|
||||
run: |
|
||||
$Platform = '${{ matrix.platform }}'
|
||||
New-Item "output" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
.\scripts\refresh-integrity-tree.ps1 -Path $PWD/unigetui_bin -FailOnUnexpectedFiles
|
||||
|
||||
# Zip
|
||||
Compress-Archive -Path "unigetui_bin/*" -DestinationPath "output/UniGetUI.$Platform.zip" -CompressionLevel Optimal
|
||||
|
||||
# Installer is created in output during the previous step
|
||||
|
||||
- name: Capture artifact sizes
|
||||
shell: pwsh
|
||||
run: |
|
||||
$Platform = '${{ matrix.platform }}'
|
||||
$SizesFile = "output/sizes.$Platform.json"
|
||||
$UncompressedSize = (Get-ChildItem "unigetui_bin" -Recurse -File | Measure-Object -Property Length -Sum).Sum
|
||||
$Artifacts = Get-ChildItem "output" -File | Where-Object { $_.Name -ne "sizes.$Platform.json" } | ForEach-Object {
|
||||
@{ Name = $_.Name; SizeBytes = $_.Length }
|
||||
}
|
||||
$Report = @{
|
||||
Platform = $Platform
|
||||
RuntimeIdentifier = "win-$Platform"
|
||||
UncompressedSizeBytes = $UncompressedSize
|
||||
Artifacts = $Artifacts
|
||||
Timestamp = [DateTimeOffset]::UtcNow.ToString("o")
|
||||
}
|
||||
$Report | ConvertTo-Json -Depth 4 | Set-Content $SizesFile -Encoding UTF8
|
||||
Write-Host "Artifact sizes saved to $SizesFile"
|
||||
Get-Content $SizesFile
|
||||
|
||||
- name: Code-sign installer
|
||||
if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$Platform = '${{ matrix.platform }}'
|
||||
.\scripts\sign.ps1 `
|
||||
-InstallerPath "output/UniGetUI.Installer.$Platform.exe" `
|
||||
-AzureTenantId '${{ secrets.AZURE_TENANT_ID }}' `
|
||||
-KeyVaultUrl '${{ secrets.CODE_SIGNING_KEYVAULT_URL }}' `
|
||||
-ClientId '${{ secrets.CODE_SIGNING_CLIENT_ID }}' `
|
||||
-ClientSecret '${{ secrets.CODE_SIGNING_CLIENT_SECRET }}' `
|
||||
-CertificateName '${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}' `
|
||||
-TimestampServer '${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}'
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: UniGetUI-release-${{ matrix.platform }}
|
||||
path: output/*
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
shell: pwsh
|
||||
run: |
|
||||
Remove-Item "unigetui_bin" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
build-avalonia:
|
||||
name: Build (${{ matrix.name }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [preflight]
|
||||
environment: ${{ needs.preflight.outputs.package-env }}
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
UNIGETUI_GITHUB_CLIENT_ID: ${{ secrets.UNIGETUI_GITHUB_CLIENT_ID }}
|
||||
UNIGETUI_GITHUB_CLIENT_SECRET: ${{ secrets.UNIGETUI_GITHUB_CLIENT_SECRET }}
|
||||
UNIGETUI_OPENSEARCH_USERNAME: ${{ secrets.UNIGETUI_OPENSEARCH_USERNAME }}
|
||||
UNIGETUI_OPENSEARCH_PASSWORD: ${{ secrets.UNIGETUI_OPENSEARCH_PASSWORD }}
|
||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||
SIGNING_IDENTITY: 'Developer ID Application: Devolutions inc.'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
name: macos-arm64
|
||||
runtime: osx-arm64
|
||||
publish_profile: osx-arm64-NativeAot
|
||||
- os: macos-latest
|
||||
name: macos-x64
|
||||
runtime: osx-x64
|
||||
publish_profile: osx-x64-NativeAot
|
||||
- os: ubuntu-latest
|
||||
name: linux-x64
|
||||
runtime: linux-x64
|
||||
publish_profile: linux-x64-NativeAot
|
||||
- os: ubuntu-latest
|
||||
name: linux-arm64
|
||||
runtime: linux-arm64
|
||||
publish_profile: linux-arm64-NativeAot
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ env.NUGET_PACKAGES }}
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('global.json', 'src/**/*.csproj', 'src/**/*.props', 'src/**/*.targets', 'src/**/*.sln', 'src/**/*.slnx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
- name: Install Linux arm64 NativeAOT toolchain
|
||||
if: matrix.runtime == 'linux-arm64'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
. /etc/os-release
|
||||
sudo dpkg --add-architecture arm64
|
||||
|
||||
# Keep the runner's default Ubuntu feeds bound to amd64, then add the
|
||||
# Ubuntu Ports arm64 feed that provides the target CRT and zlib objects.
|
||||
if compgen -G "/etc/apt/sources.list.d/*.sources" > /dev/null; then
|
||||
for source in /etc/apt/sources.list.d/*.sources; do
|
||||
if ! grep -q '^Architectures:' "$source"; then
|
||||
sudo sed -i '/^Types: deb$/a Architectures: amd64' "$source"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -f /etc/apt/sources.list ]; then
|
||||
sudo sed -i -E '/^deb /s/^deb /deb [arch=amd64] /' /etc/apt/sources.list
|
||||
fi
|
||||
|
||||
sudo tee /etc/apt/sources.list.d/ubuntu-arm64.sources >/dev/null <<EOF
|
||||
Types: deb
|
||||
URIs: http://ports.ubuntu.com/ubuntu-ports/
|
||||
Suites: ${VERSION_CODENAME} ${VERSION_CODENAME}-updates ${VERSION_CODENAME}-backports
|
||||
Components: main restricted universe multiverse
|
||||
Architectures: arm64
|
||||
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
|
||||
Types: deb
|
||||
URIs: http://ports.ubuntu.com/ubuntu-ports/
|
||||
Suites: ${VERSION_CODENAME}-security
|
||||
Components: main restricted universe multiverse
|
||||
Architectures: arm64
|
||||
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
EOF
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
clang \
|
||||
llvm \
|
||||
binutils-aarch64-linux-gnu \
|
||||
gcc-aarch64-linux-gnu \
|
||||
zlib1g-dev:arm64
|
||||
|
||||
- name: Set version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
|
||||
./scripts/set-version.ps1 -Version $PackageVersion
|
||||
|
||||
- name: Restore dependencies
|
||||
working-directory: src
|
||||
run: dotnet restore UniGetUI.Avalonia/UniGetUI.Avalonia.csproj --runtime ${{ matrix.runtime }}
|
||||
|
||||
- name: Publish
|
||||
working-directory: src
|
||||
shell: pwsh
|
||||
run: |
|
||||
$PublishProfile = '${{ matrix.publish_profile }}'
|
||||
$PublishArgs = @(
|
||||
'publish',
|
||||
'UniGetUI.Avalonia/UniGetUI.Avalonia.csproj',
|
||||
'--configuration', 'Release',
|
||||
'--runtime', '${{ matrix.runtime }}',
|
||||
'--self-contained', 'true',
|
||||
'--output', '../bin/${{ matrix.name }}'
|
||||
)
|
||||
if ($PublishProfile) {
|
||||
$PublishArgs += "/p:PublishProfile=$PublishProfile"
|
||||
}
|
||||
dotnet @PublishArgs
|
||||
|
||||
- name: Remove oversized Linux debug symbols
|
||||
if: runner.os == 'Linux'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$PublishDir = "bin/${{ matrix.name }}"
|
||||
$MaxDebugSymbolSizeBytes = 1MB
|
||||
$DebugSymbolsToRemove = @(Get-ChildItem $PublishDir -Include "*.dbg", "*.pdb" -File -Recurse | Where-Object {
|
||||
$_.Length -gt $MaxDebugSymbolSizeBytes
|
||||
})
|
||||
|
||||
if ($DebugSymbolsToRemove.Count -gt 0) {
|
||||
$RemovedDebugSymbolBytes = ($DebugSymbolsToRemove | Measure-Object -Property Length -Sum).Sum
|
||||
$DebugSymbolsToRemove | Remove-Item -Force
|
||||
Write-Host ("Removed {0} oversized Linux debug symbols above {1:N2} MiB ({2:N2} MiB total)." -f $DebugSymbolsToRemove.Count, ($MaxDebugSymbolSizeBytes / 1MB), ($RemovedDebugSymbolBytes / 1MB))
|
||||
}
|
||||
|
||||
- name: Install macOS packaging tools
|
||||
if: runner.os == 'macOS'
|
||||
run: brew install create-dmg
|
||||
|
||||
- name: Check out Devolutions/actions
|
||||
if: ${{ runner.os == 'macOS' && fromJSON(needs.preflight.outputs.dry-run) == false }}
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: Devolutions/actions
|
||||
ref: v1
|
||||
token: ${{ secrets.DEVOLUTIONSBOT_TOKEN }}
|
||||
path: ./.github/workflows
|
||||
|
||||
- name: Import Developer ID certificate
|
||||
if: ${{ runner.os == 'macOS' && fromJSON(needs.preflight.outputs.dry-run) == false }}
|
||||
uses: ./.github/workflows/macos-code-sign-setup
|
||||
with:
|
||||
base64_certificate: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE }}
|
||||
certificate_password: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD }}
|
||||
|
||||
- name: Package (macOS)
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION='${{ needs.preflight.outputs.package-version }}'
|
||||
APP_BUNDLE="UniGetUI.app"
|
||||
APP_EXECUTABLE="UniGetUI"
|
||||
DRY_RUN='${{ needs.preflight.outputs.dry-run }}'
|
||||
|
||||
rm -rf "${APP_BUNDLE}" output
|
||||
mkdir -p output
|
||||
mkdir -p "${APP_BUNDLE}/Contents/MacOS"
|
||||
mkdir -p "${APP_BUNDLE}/Contents/Resources"
|
||||
|
||||
cp -R "bin/${{ matrix.name }}/." "${APP_BUNDLE}/Contents/MacOS/"
|
||||
find "${APP_BUNDLE}/Contents/MacOS" -name "*.pdb" -type f -delete
|
||||
chmod +x "${APP_BUNDLE}/Contents/MacOS/${APP_EXECUTABLE}"
|
||||
sed "s|@VERSION@|${VERSION}|g" \
|
||||
scripts/macos/Info.plist \
|
||||
> "${APP_BUNDLE}/Contents/Info.plist"
|
||||
# Compile the appearance-aware app icon (Default/Dark/Tinted/Clear) from the Icon Composer
|
||||
# .icon into the bundle: produces Assets.car + AppIcon.icns. Info.plist already declares
|
||||
# CFBundleIconName=AppIcon, so macOS renders the appearance styles itself (incl. when closed).
|
||||
xcrun actool scripts/macos/AppIcon.icon \
|
||||
--compile "${APP_BUNDLE}/Contents/Resources" \
|
||||
--app-icon AppIcon \
|
||||
--output-partial-info-plist "$(mktemp).plist" \
|
||||
--platform macosx \
|
||||
--minimum-deployment-target 12.0 \
|
||||
--errors --warnings
|
||||
# Static .icns fallback for macOS < 26 (CFBundleIconFile) and the dmg volume icon.
|
||||
cp scripts/macos/UniGetUI.icns "${APP_BUNDLE}/Contents/Resources/"
|
||||
|
||||
if [ "$DRY_RUN" = "false" ]; then
|
||||
ENTITLEMENTS="scripts/macos/Entitlements.plist"
|
||||
find "${APP_BUNDLE}/Contents/MacOS" -type f \! -name "${APP_EXECUTABLE}" -print0 \
|
||||
| while IFS= read -r -d '' f; do
|
||||
codesign --force --timestamp --options runtime \
|
||||
--entitlements "$ENTITLEMENTS" \
|
||||
--sign "$SIGNING_IDENTITY" "$f"
|
||||
done
|
||||
codesign --force --timestamp --options runtime \
|
||||
--entitlements "$ENTITLEMENTS" \
|
||||
--sign "$SIGNING_IDENTITY" "${APP_BUNDLE}"
|
||||
codesign --verify --verbose=2 "${APP_BUNDLE}"
|
||||
else
|
||||
find "${APP_BUNDLE}/Contents/MacOS" -type f | while IFS= read -r f; do
|
||||
codesign --force --sign - "$f" 2>/dev/null || true
|
||||
done
|
||||
codesign --force --sign - "${APP_BUNDLE}"
|
||||
fi
|
||||
|
||||
COPYFILE_DISABLE=1 tar -czf "output/UniGetUI.${{ matrix.name }}.tar.gz" "${APP_BUNDLE}"
|
||||
|
||||
create-dmg \
|
||||
--volname "UniGetUI" \
|
||||
--volicon "scripts/macos/UniGetUI.icns" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 600 400 \
|
||||
--icon-size 128 \
|
||||
--icon "UniGetUI.app" 150 185 \
|
||||
--hide-extension "UniGetUI.app" \
|
||||
--app-drop-link 450 185 \
|
||||
"output/UniGetUI.${{ matrix.name }}.dmg" \
|
||||
"${APP_BUNDLE}"
|
||||
|
||||
test -f "output/UniGetUI.${{ matrix.name }}.tar.gz"
|
||||
test -f "output/UniGetUI.${{ matrix.name }}.dmg"
|
||||
|
||||
- name: Sign DMG
|
||||
if: ${{ runner.os == 'macOS' && fromJSON(needs.preflight.outputs.dry-run) == false }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DMG="output/UniGetUI.${{ matrix.name }}.dmg"
|
||||
codesign --force --timestamp --sign "$SIGNING_IDENTITY" "$DMG"
|
||||
codesign --verify --verbose=2 "$DMG"
|
||||
|
||||
- name: Notarize DMG
|
||||
if: ${{ runner.os == 'macOS' && fromJSON(needs.preflight.outputs.dry-run) == false }}
|
||||
uses: ./.github/workflows/macos-notarize
|
||||
timeout-minutes: 15
|
||||
with:
|
||||
apple_password: ${{ secrets.APPLE_BOT_PASSWORD }}
|
||||
package_path: output/UniGetUI.${{ matrix.name }}.dmg
|
||||
|
||||
- name: Install Linux packaging tools
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm
|
||||
|
||||
- name: Package (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$Version = '${{ needs.preflight.outputs.package-version }}'
|
||||
$Parts = $Version -split '-', 2
|
||||
$BaseVersion = $Parts[0]
|
||||
$Prerelease = if ($Parts.Count -gt 1) { $Parts[1] } else { $null }
|
||||
|
||||
# deb: use tilde notation for pre-releases (Ubuntu 18.04+ compatible)
|
||||
$DebVersion = if ($Prerelease) { "${BaseVersion}~${Prerelease}" } else { $Version }
|
||||
# rpm: pre-release encoded in Release/Iteration field (RHEL 8+ / RPM 4.14+)
|
||||
$RpmVersion = $BaseVersion
|
||||
$RpmIteration = if ($Prerelease) { "0.${Prerelease}" } else { '1' }
|
||||
|
||||
# Map .NET RID -> package arch names
|
||||
$DebArch, $RpmArch = switch ('${{ matrix.runtime }}') {
|
||||
'linux-arm64' { 'arm64', 'aarch64' }
|
||||
default { 'amd64', 'x86_64' }
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path output | Out-Null
|
||||
|
||||
# .tar.gz
|
||||
& tar -czf "output/UniGetUI.${{ matrix.name }}.tar.gz" -C "bin/${{ matrix.name }}" .
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
|
||||
# .deb — Ubuntu 18.04+ (glibc 2.27, Debian policy 3.9.6)
|
||||
& ./scripts/package-linux.ps1 `
|
||||
-PackageType deb `
|
||||
-SourceDir "bin/${{ matrix.name }}" `
|
||||
-OutputPath "output/UniGetUI.${{ matrix.name }}.deb" `
|
||||
-Version $DebVersion `
|
||||
-Architecture $DebArch
|
||||
|
||||
# .rpm — RHEL 8+ (RPM 4.14, glibc 2.28)
|
||||
& ./scripts/package-linux.ps1 `
|
||||
-PackageType rpm `
|
||||
-SourceDir "bin/${{ matrix.name }}" `
|
||||
-OutputPath "output/UniGetUI.${{ matrix.name }}.rpm" `
|
||||
-Version $RpmVersion `
|
||||
-Iteration $RpmIteration `
|
||||
-Architecture $RpmArch
|
||||
|
||||
- name: Capture artifact sizes
|
||||
shell: pwsh
|
||||
run: |
|
||||
$Name = '${{ matrix.name }}'
|
||||
$Runtime = '${{ matrix.runtime }}'
|
||||
$SizesFile = "output/sizes.$Name.json"
|
||||
$SourceDir = if ('${{ runner.os }}' -eq 'macOS') { "UniGetUI.app/Contents/MacOS" } else { "bin/$Name" }
|
||||
$UncompressedSize = (Get-ChildItem $SourceDir -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
|
||||
$Artifacts = Get-ChildItem "output" -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "sizes.$Name.json" } | ForEach-Object {
|
||||
@{ Name = $_.Name; SizeBytes = $_.Length }
|
||||
}
|
||||
$Report = @{
|
||||
Platform = $Name
|
||||
RuntimeIdentifier = $Runtime
|
||||
UncompressedSizeBytes = $UncompressedSize
|
||||
Artifacts = $Artifacts
|
||||
Timestamp = [DateTimeOffset]::UtcNow.ToString("o")
|
||||
}
|
||||
$Report | ConvertTo-Json -Depth 4 | Set-Content $SizesFile -Encoding UTF8
|
||||
Write-Host "Artifact sizes saved to $SizesFile"
|
||||
Get-Content $SizesFile
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: UniGetUI-${{ matrix.name }}
|
||||
path: output/*
|
||||
|
||||
publish:
|
||||
name: Publish GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [preflight, build, build-avalonia]
|
||||
if: ${{ fromJSON(needs.preflight.outputs.skip-publish) == false }}
|
||||
environment: ${{ needs.preflight.outputs.package-env }}
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: output
|
||||
|
||||
- name: Add legacy installer filename
|
||||
shell: pwsh
|
||||
working-directory: output
|
||||
run: |
|
||||
$InstallerFiles = Get-ChildItem -Path . -Recurse -File -Filter "UniGetUI.Installer.x64.exe"
|
||||
if (-not $InstallerFiles) {
|
||||
throw "Could not find UniGetUI.Installer.x64.exe in downloaded artifacts"
|
||||
}
|
||||
|
||||
$InstallerFiles | ForEach-Object {
|
||||
$LegacyInstallerPath = Join-Path $_.DirectoryName "UniGetUI.Installer.exe"
|
||||
Copy-Item -Path $_.FullName -Destination $LegacyInstallerPath -Force
|
||||
Write-Host "Created legacy installer alias: $LegacyInstallerPath"
|
||||
}
|
||||
|
||||
- name: Generate consolidated checksums
|
||||
shell: pwsh
|
||||
working-directory: output
|
||||
run: |
|
||||
$ChecksumFile = Join-Path $PWD "checksums.txt"
|
||||
$ChecksumLines = Get-ChildItem -Path . -Recurse -File | Where-Object {
|
||||
$_.Name -notmatch '^checksums(\..+)?\.txt$'
|
||||
} | Sort-Object Name | ForEach-Object {
|
||||
$hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash
|
||||
"$hash $($_.Name)"
|
||||
}
|
||||
|
||||
Set-Content -Path $ChecksumFile -Value $ChecksumLines -Encoding utf8NoBOM
|
||||
|
||||
echo "::group::checksums"
|
||||
Get-Content $ChecksumFile
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Generate consolidated size report
|
||||
shell: pwsh
|
||||
working-directory: output
|
||||
run: |
|
||||
$SizeFiles = Get-ChildItem -Path . -Recurse -File -Filter "sizes.*.json" | Sort-Object Name
|
||||
if (-not $SizeFiles) {
|
||||
Write-Host "No size files found."
|
||||
return
|
||||
}
|
||||
|
||||
$Rows = foreach ($file in $SizeFiles) {
|
||||
$data = Get-Content $file.FullName -Raw | ConvertFrom-Json
|
||||
$uncompressedMiB = [math]::Round($data.UncompressedSizeBytes / 1MB, 2)
|
||||
foreach ($artifact in $data.Artifacts) {
|
||||
[PSCustomObject]@{
|
||||
Platform = $data.Platform
|
||||
RID = $data.RuntimeIdentifier
|
||||
Artifact = $artifact.Name
|
||||
CompressedMiB = [math]::Round($artifact.SizeBytes / 1MB, 2)
|
||||
UncompressedMiB = $uncompressedMiB
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$SizeReportFile = Join-Path $PWD "sizes.md"
|
||||
$Lines = @("# UniGetUI Release Artifact Sizes", "")
|
||||
$Lines += $Rows | Format-Table -AutoSize | Out-String -Stream | Where-Object { $_.Trim() -ne '' }
|
||||
Set-Content -Path $SizeReportFile -Value $Lines -Encoding utf8NoBOM
|
||||
|
||||
echo "::group::sizes"
|
||||
Get-Content $SizeReportFile
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Create GitHub Release
|
||||
shell: pwsh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
working-directory: output
|
||||
run: |
|
||||
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
|
||||
$DraftRelease = [System.Boolean]::Parse('${{ needs.preflight.outputs.draft-release }}')
|
||||
$DryRun = [System.Boolean]::Parse('${{ needs.preflight.outputs.dry-run }}')
|
||||
|
||||
echo "::group::checksums"
|
||||
Get-Content "./checksums.txt"
|
||||
echo "::endgroup::"
|
||||
|
||||
$ReleaseTag = "v$PackageVersion"
|
||||
$ReleaseTitle = "UniGetUI v${PackageVersion}"
|
||||
$Repository = $Env:GITHUB_REPOSITORY
|
||||
$DraftArg = if ($DraftRelease) { '--draft' } else { $null }
|
||||
|
||||
$Files = Get-ChildItem -Path . -Recurse -File | Where-Object {
|
||||
$_.Name -eq 'checksums.txt' -or $_.Name -notmatch '^checksums\..+\.txt$'
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "Dry Run: skipping GitHub release creation!"
|
||||
Write-Host "Would create release $ReleaseTag with title '$ReleaseTitle' (draft=$DraftRelease)"
|
||||
$Files | ForEach-Object { Write-Host " - $($_.FullName)" }
|
||||
} else {
|
||||
if ($DraftArg) {
|
||||
& gh release create $ReleaseTag --repo $Repository --title $ReleaseTitle $DraftArg $Files.FullName
|
||||
} else {
|
||||
& gh release create $ReleaseTag --repo $Repository --title $ReleaseTitle $Files.FullName
|
||||
}
|
||||
}
|
||||
|
||||
- name: Check out Devolutions/actions
|
||||
if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }}
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: Devolutions/actions
|
||||
ref: v1
|
||||
token: ${{ secrets.DEVOLUTIONSBOT_TOKEN }}
|
||||
path: ./.github/workflows
|
||||
|
||||
- name: Install Devolutions Toolbox
|
||||
if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }}
|
||||
uses: ./.github/workflows/toolbox-install
|
||||
with:
|
||||
github_token: ${{ secrets.DEVOLUTIONSBOT_TOKEN }}
|
||||
|
||||
- name: Stage files for OneDrive upload
|
||||
if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
|
||||
New-Item -Path "onedrive-staging" -ItemType Directory -Force | Out-Null
|
||||
|
||||
$OneDriveVersion = '${{ needs.preflight.outputs.onedrive-version }}'
|
||||
|
||||
$Mappings = @{
|
||||
"output/UniGetUI-release-x64/UniGetUI.Installer.x64.exe" = "Devolutions.UniGetUI.win-x64.$OneDriveVersion.exe"
|
||||
"output/UniGetUI-release-arm64/UniGetUI.Installer.arm64.exe" = "Devolutions.UniGetUI.win-arm64.$OneDriveVersion.exe"
|
||||
"output/UniGetUI-release-x64/UniGetUI.x64.zip" = "Devolutions.UniGetUI.win-x64.$OneDriveVersion.zip"
|
||||
"output/UniGetUI-release-arm64/UniGetUI.arm64.zip" = "Devolutions.UniGetUI.win-arm64.$OneDriveVersion.zip"
|
||||
"output/UniGetUI-macos-x64/UniGetUI.macos-x64.dmg" = "Devolutions.UniGetUI.macos-x64.$OneDriveVersion.dmg"
|
||||
"output/UniGetUI-macos-arm64/UniGetUI.macos-arm64.dmg" = "Devolutions.UniGetUI.macos-arm64.$OneDriveVersion.dmg"
|
||||
"output/UniGetUI-macos-x64/UniGetUI.macos-x64.tar.gz" = "Devolutions.UniGetUI.macos-x64.$OneDriveVersion.tar.gz"
|
||||
"output/UniGetUI-macos-arm64/UniGetUI.macos-arm64.tar.gz" = "Devolutions.UniGetUI.macos-arm64.$OneDriveVersion.tar.gz"
|
||||
"output/UniGetUI-linux-x64/UniGetUI.linux-x64.deb" = "Devolutions.UniGetUI.linux-x64.$OneDriveVersion.deb"
|
||||
"output/UniGetUI-linux-arm64/UniGetUI.linux-arm64.deb" = "Devolutions.UniGetUI.linux-arm64.$OneDriveVersion.deb"
|
||||
"output/UniGetUI-linux-x64/UniGetUI.linux-x64.rpm" = "Devolutions.UniGetUI.linux-x64.$OneDriveVersion.rpm"
|
||||
"output/UniGetUI-linux-arm64/UniGetUI.linux-arm64.rpm" = "Devolutions.UniGetUI.linux-arm64.$OneDriveVersion.rpm"
|
||||
"output/UniGetUI-linux-x64/UniGetUI.linux-x64.tar.gz" = "Devolutions.UniGetUI.linux-x64.$OneDriveVersion.tar.gz"
|
||||
"output/UniGetUI-linux-arm64/UniGetUI.linux-arm64.tar.gz" = "Devolutions.UniGetUI.linux-arm64.$OneDriveVersion.tar.gz"
|
||||
}
|
||||
|
||||
foreach ($entry in $Mappings.GetEnumerator()) {
|
||||
if (-not (Test-Path $entry.Key)) {
|
||||
throw "File not found: $($entry.Key)"
|
||||
}
|
||||
Copy-Item -Path $entry.Key -Destination "onedrive-staging/$($entry.Value)"
|
||||
Write-Host "Staged: $($entry.Key) -> $($entry.Value)"
|
||||
}
|
||||
|
||||
- name: Upload to OneDrive
|
||||
if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }}
|
||||
uses: ./.github/workflows/onedrive-upload
|
||||
with:
|
||||
azure_client_id: ${{ secrets.ONEDRIVE_AUTOMATION_CLIENT_ID }}
|
||||
azure_client_secret: ${{ secrets.ONEDRIVE_AUTOMATION_CLIENT_SECRET }}
|
||||
conflict_behavior: replace
|
||||
destination_path: /UniGetUI/${{ needs.preflight.outputs.onedrive-version }}
|
||||
remote: releases
|
||||
source_path: Devolutions.UniGetUI.*
|
||||
working_directory: onedrive-staging
|
||||
|
||||
size-report:
|
||||
name: Consolidated Size Report
|
||||
runs-on: ubuntu-latest
|
||||
needs: [preflight, build, build-avalonia]
|
||||
if: always() && needs.build.result != 'cancelled' && needs.build-avalonia.result != 'cancelled'
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: output
|
||||
|
||||
- name: Generate consolidated size report
|
||||
shell: pwsh
|
||||
working-directory: output
|
||||
run: |
|
||||
$SizeFiles = Get-ChildItem -Path . -Recurse -File -Filter "sizes.*.json" | Sort-Object Name
|
||||
if (-not $SizeFiles) {
|
||||
Write-Host "No size files found."
|
||||
return
|
||||
}
|
||||
|
||||
$Rows = foreach ($file in $SizeFiles) {
|
||||
$data = Get-Content $file.FullName -Raw | ConvertFrom-Json
|
||||
$uncompressedMiB = [math]::Round($data.UncompressedSizeBytes / 1MB, 2)
|
||||
foreach ($artifact in $data.Artifacts) {
|
||||
[PSCustomObject]@{
|
||||
Platform = $data.Platform
|
||||
RID = $data.RuntimeIdentifier
|
||||
Artifact = $artifact.Name
|
||||
CompressedMiB = [math]::Round($artifact.SizeBytes / 1MB, 2)
|
||||
UncompressedMiB = $uncompressedMiB
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$SizeReportFile = Join-Path $PWD "sizes.md"
|
||||
$Lines = @("# UniGetUI Release Artifact Sizes", "")
|
||||
$Lines += $Rows | Format-Table -AutoSize | Out-String -Stream | Where-Object { $_.Trim() -ne '' }
|
||||
Set-Content -Path $SizeReportFile -Value $Lines -Encoding utf8NoBOM
|
||||
|
||||
echo "::group::sizes"
|
||||
Get-Content $SizeReportFile
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Upload size report
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: UniGetUI-size-report
|
||||
path: output/sizes.md
|
||||
@@ -0,0 +1,179 @@
|
||||
name: CLI Headless E2E
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/**/*.cs'
|
||||
- 'src/**/*.csproj'
|
||||
- 'src/**/*.props'
|
||||
- 'src/**/*.targets'
|
||||
- 'src/**/*.sln'
|
||||
- 'src/**/*.slnx'
|
||||
- 'testing/automation/**'
|
||||
- '.github/workflows/cli-headless-e2e.yml'
|
||||
- 'global.json'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
cli-headless-e2e:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Windows (Avalonia)
|
||||
os: windows-latest
|
||||
solution: UniGetUI.Windows.slnx
|
||||
daemon_project: UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
|
||||
daemon_build_args: '-p:Platform=x64'
|
||||
manifest: testing/automation/cli-e2e.manifest.windows.json
|
||||
- name: Windows (NativeAOT)
|
||||
os: windows-latest
|
||||
solution: UniGetUI.Windows.slnx
|
||||
daemon_project: UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
|
||||
daemon_publish_profile: Win-x64-NativeAot
|
||||
daemon_build_args: '-p:Platform=x64'
|
||||
manifest: testing/automation/cli-e2e.manifest.windows.json
|
||||
- name: Linux (Avalonia)
|
||||
os: ubuntu-latest
|
||||
solution: UniGetUI.Avalonia.slnx
|
||||
daemon_project: UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
|
||||
daemon_build_args: ''
|
||||
manifest: testing/automation/cli-e2e.manifest.linux.json
|
||||
- name: Linux (NativeAOT)
|
||||
os: ubuntu-latest
|
||||
solution: UniGetUI.Avalonia.slnx
|
||||
daemon_project: UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
|
||||
daemon_publish_profile: linux-x64-NativeAot
|
||||
daemon_build_args: ''
|
||||
manifest: testing/automation/cli-e2e.manifest.linux-nativeaot.json
|
||||
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CONFIGURATION: Release
|
||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ env.NUGET_PACKAGES }}
|
||||
key: ${{ runner.os }}-nuget-e2e-${{ hashFiles('global.json', 'src/**/*.csproj', 'src/**/*.props', 'src/**/*.targets', 'src/**/*.sln', 'src/**/*.slnx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-e2e-
|
||||
|
||||
- name: Restore solution
|
||||
working-directory: src
|
||||
shell: pwsh
|
||||
run: dotnet restore ${{ matrix.solution }}
|
||||
|
||||
- name: Build headless daemon
|
||||
working-directory: src
|
||||
shell: pwsh
|
||||
run: |
|
||||
$PublishProfile = '${{ matrix.daemon_publish_profile }}'
|
||||
$BuildArgs = '${{ matrix.daemon_build_args }}'
|
||||
|
||||
dotnet build-server shutdown
|
||||
|
||||
if ($PublishProfile) {
|
||||
$RuntimeIdentifier = $PublishProfile -replace '-NativeAot$', '' -replace '^Win-', 'win-' -replace '^linux-', 'linux-' -replace '^osx-', 'osx-'
|
||||
$OutputPath = Join-Path (Resolve-Path '..') "artifacts/e2e-nativeaot/$RuntimeIdentifier"
|
||||
New-Item -ItemType Directory -Force -Path $OutputPath | Out-Null
|
||||
|
||||
$args = @(
|
||||
'publish',
|
||||
'${{ matrix.daemon_project }}',
|
||||
'--no-restore',
|
||||
'--configuration',
|
||||
'${{ env.CONFIGURATION }}',
|
||||
'--runtime',
|
||||
$RuntimeIdentifier,
|
||||
'--self-contained',
|
||||
'true',
|
||||
'--output',
|
||||
$OutputPath,
|
||||
'--verbosity',
|
||||
'minimal',
|
||||
'/p:UseSharedCompilation=false',
|
||||
'/m:1',
|
||||
"/p:PublishProfile=$PublishProfile"
|
||||
)
|
||||
if ($BuildArgs) {
|
||||
$args += $BuildArgs
|
||||
}
|
||||
dotnet @args
|
||||
|
||||
$exeName = if ('${{ runner.os }}' -eq 'Windows') { 'UniGetUI.exe' } else { 'UniGetUI' }
|
||||
$daemonExe = Join-Path $OutputPath $exeName
|
||||
if (-not (Test-Path $daemonExe)) {
|
||||
throw "NativeAOT daemon executable was not produced at $daemonExe"
|
||||
}
|
||||
if ('${{ runner.os }}' -ne 'Windows') {
|
||||
chmod +x $daemonExe
|
||||
}
|
||||
echo "UNIGETUI_DAEMON_EXE=$daemonExe" >> $Env:GITHUB_ENV
|
||||
}
|
||||
else {
|
||||
$args = @(
|
||||
'build',
|
||||
'${{ matrix.daemon_project }}',
|
||||
'--no-restore',
|
||||
'--configuration',
|
||||
'${{ env.CONFIGURATION }}',
|
||||
'--verbosity',
|
||||
'minimal',
|
||||
'/p:UseSharedCompilation=false',
|
||||
'/m:1'
|
||||
)
|
||||
if ($BuildArgs) {
|
||||
$args += $BuildArgs
|
||||
}
|
||||
dotnet @args
|
||||
}
|
||||
|
||||
- name: Upgrade pip tooling
|
||||
shell: pwsh
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
|
||||
- name: Show package-manager inventory
|
||||
shell: pwsh
|
||||
run: |
|
||||
dotnet --version
|
||||
python --version
|
||||
python -m pip --version
|
||||
npm --version
|
||||
|
||||
- name: Run headless CLI E2E
|
||||
shell: pwsh
|
||||
env:
|
||||
UNIGETUI_CLI_E2E_MANIFEST: ${{ matrix.manifest }}
|
||||
UNIGETUI_CLI_E2E_ARTIFACTS: ${{ github.workspace }}/artifacts/cli-headless-e2e/${{ runner.os }}
|
||||
run: ./testing/automation/run-cli-e2e.ps1
|
||||
|
||||
- name: Upload CLI E2E artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cli-headless-e2e-${{ matrix.name }}
|
||||
path: artifacts/cli-headless-e2e/${{ runner.os }}
|
||||
if-no-files-found: warn
|
||||
@@ -0,0 +1,90 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: csharp
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -0,0 +1,115 @@
|
||||
name: .NET Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'global.json'
|
||||
- '**.cs'
|
||||
- '**.axaml'
|
||||
- '**.csproj'
|
||||
- '**.props'
|
||||
- '**.targets'
|
||||
- '**.sln'
|
||||
- '**.slnx'
|
||||
- 'src/.editorconfig'
|
||||
- '.github/workflows/dotnet-test.yml'
|
||||
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'global.json'
|
||||
- '**.cs'
|
||||
- '**.axaml'
|
||||
- '**.csproj'
|
||||
- '**.props'
|
||||
- '**.targets'
|
||||
- '**.sln'
|
||||
- '**.slnx'
|
||||
- 'src/.editorconfig'
|
||||
- '.github/workflows/dotnet-test.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-codebase:
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install .NET SDK
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ env.NUGET_PACKAGES }}
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('global.json', 'src/**/*.csproj', 'src/**/*.props', 'src/**/*.targets', 'src/**/*.sln', 'src/**/*.slnx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: src
|
||||
run: dotnet restore UniGetUI.Windows.slnx
|
||||
|
||||
- name: Check whitespace formatting
|
||||
run: dotnet format whitespace src --folder --verify-no-changes --verbosity minimal
|
||||
|
||||
- name: Check code style formatting (Avalonia Windows solution)
|
||||
working-directory: src
|
||||
run: dotnet format style UniGetUI.Windows.slnx --no-restore --verify-no-changes --verbosity minimal
|
||||
|
||||
- name: Build Avalonia Windows solution
|
||||
working-directory: src
|
||||
run: |
|
||||
dotnet build-server shutdown
|
||||
dotnet build UniGetUI.Windows.slnx --no-restore --verbosity minimal /p:Platform=x64 /p:UseSharedCompilation=false /m:1
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: src
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: dotnet test UniGetUI.Windows.slnx --no-restore --verbosity q --nologo /p:Platform=x64 /p:UseSharedCompilation=false /m:1
|
||||
|
||||
- name: Report full-trim publish warnings
|
||||
working-directory: src
|
||||
shell: pwsh
|
||||
continue-on-error: true
|
||||
run: |
|
||||
dotnet build-server shutdown
|
||||
dotnet publish UniGetUI.Avalonia/UniGetUI.Avalonia.csproj `
|
||||
--no-restore `
|
||||
--configuration Release `
|
||||
--runtime win-x64 `
|
||||
--self-contained true `
|
||||
--output "${{ runner.temp }}\unigetui-full-trim-publish" `
|
||||
/p:Platform=x64 `
|
||||
/p:TrimMode=full `
|
||||
/p:TrimmerSingleWarn=false `
|
||||
/p:UseSharedCompilation=false `
|
||||
--verbosity minimal
|
||||
|
||||
- name: Report NativeAOT publish warnings
|
||||
working-directory: src
|
||||
shell: pwsh
|
||||
run: |
|
||||
dotnet build-server shutdown
|
||||
dotnet publish UniGetUI.Avalonia/UniGetUI.Avalonia.csproj `
|
||||
--no-restore `
|
||||
--configuration Release `
|
||||
--runtime win-x64 `
|
||||
--self-contained true `
|
||||
--output "${{ runner.temp }}\unigetui-nativeaot-publish" `
|
||||
/m:1 `
|
||||
/p:Platform=x64 `
|
||||
/p:PublishProfile=Win-x64-NativeAot `
|
||||
/p:TrimmerSingleWarn=false `
|
||||
/p:UseSharedCompilation=false `
|
||||
--verbosity minimal
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Translation Validation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'scripts/translation/**'
|
||||
- '.agents/skills/translation-*/scripts/**'
|
||||
- 'src/Languages/**'
|
||||
- 'src/Languages/Data/LanguagesReference.json'
|
||||
- 'src/Languages/Data/TranslatedPercentages.json'
|
||||
- 'src/Languages/Data/Translators.json'
|
||||
- 'src/UniGetUI.Core.Data/Assets/Data/Contributors.list'
|
||||
- '.github/workflows/translation-validation.yml'
|
||||
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'scripts/translation/**'
|
||||
- '.agents/skills/translation-*/scripts/**'
|
||||
- 'src/Languages/**'
|
||||
- 'src/Languages/Data/LanguagesReference.json'
|
||||
- 'src/Languages/Data/TranslatedPercentages.json'
|
||||
- 'src/Languages/Data/Translators.json'
|
||||
- 'src/UniGetUI.Core.Data/Assets/Data/Contributors.list'
|
||||
- '.github/workflows/translation-validation.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
validate-translations:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Validate translation file structure
|
||||
shell: pwsh
|
||||
run: pwsh ./scripts/translation/Verify-Translations.ps1
|
||||
|
||||
- name: Validate boundary alignment
|
||||
shell: pwsh
|
||||
run: pwsh ./scripts/translation/Set-TranslationBoundaryOrder.ps1 -CheckOnly
|
||||
@@ -0,0 +1,99 @@
|
||||
/.vscode/
|
||||
src/.vscode/
|
||||
"UniGetUI Store.exe"
|
||||
UniGetUI Store Installer.exe
|
||||
UniGetUI Installer.exe
|
||||
WingetUI Installer.exe
|
||||
UniGetUI Store.exe
|
||||
UniGetUI.exe
|
||||
installer.iss
|
||||
output/
|
||||
/artifacts/
|
||||
*.old
|
||||
vcredist.exe
|
||||
unigetui_bin/
|
||||
APIKEY.txt
|
||||
*.pyc
|
||||
WebBasedData/new_urls.txt
|
||||
WebBasedData/screenshot-database.json.backup
|
||||
pr_body.md
|
||||
*.log
|
||||
UniGetUI/wingettest.py
|
||||
*.nupkg
|
||||
|
||||
wui-1.7.0-console.zip
|
||||
wui1.7.0-debug2.zip
|
||||
env/
|
||||
.vs/*
|
||||
dllexp.exe
|
||||
sign.cmd.lnk
|
||||
src/packages/
|
||||
|
||||
src/UniGetUI/bin
|
||||
src/UniGetUI/obj
|
||||
|
||||
src/WindowsPackageManager.Interop/bin
|
||||
src/WindowsPackageManager.Interop/obj
|
||||
src/WindowsPackageManager.Interop/out
|
||||
src/WindowsPackageManager.Interop/outpublish
|
||||
|
||||
src/UniGetU*/bin
|
||||
src/UniGetU*/obj
|
||||
src/UniGetU*/out
|
||||
src/UniGetU*/outpublish
|
||||
|
||||
src/UniGetUI.Pinget.Cli/bin
|
||||
src/UniGetUI.Pinget.Cli/obj
|
||||
|
||||
src/ExternalLibraries.*/bin
|
||||
src/ExternalLibraries.*/obj
|
||||
src/ExternalLibraries.*/out
|
||||
src/ExternalLibraries.*/outpublish
|
||||
|
||||
src/.vs
|
||||
src/.vscode
|
||||
new-names.docx
|
||||
|
||||
src/UniGetUI/choco-cli/logs/
|
||||
src/UniGetUI/choco-cli/lib/
|
||||
src/UniGetUI/choco-cli/lib-bad/
|
||||
src/UniGetUI/choco-cli/.chocolatey/
|
||||
src/UniGetUI/choco-cli/lib/chocolatey-compatibility.extension/*
|
||||
src/UniGetUI/choco-cli/extensions/chocolatey-core/*
|
||||
src/UniGetUI/choco-cli/extensions/chocolatey-compatibility/*
|
||||
src/UniGetUI/choco-cli/lib-bkp/
|
||||
src/UniGetUI/choco-cli/extensions/chocolatey-windowsupdate/*
|
||||
src/UniGetUI/choco-cli/bin/chocolatey.exe
|
||||
src/UniGetUI/choco-cli/redirects/chocolatey.exe
|
||||
src/UniGetUI/choco-cli/redirects/chocolatey.exe.ignore
|
||||
src/UniGetUI/choco-cli/redirects/cinst.exe
|
||||
src/UniGetUI/choco-cli/redirects/cinst.exe.ignore
|
||||
src/UniGetUI/choco-cli/redirects/clist.exe
|
||||
src/UniGetUI/choco-cli/redirects/clist.exe.ignore
|
||||
src/UniGetUI/choco-cli/redirects/cpush.exe
|
||||
src/UniGetUI/choco-cli/redirects/cpush.exe.ignore
|
||||
src/UniGetUI/choco-cli/redirects/cuninst.exe
|
||||
src/UniGetUI/choco-cli/redirects/cuninst.exe.ignore
|
||||
src/UniGetUI/choco-cli/redirects/cup.exe
|
||||
src/UniGetUI/choco-cli/redirects/cup.exe.ignore
|
||||
src/UniGetUI/choco-cli/bin/ssh-copy-id.exe
|
||||
src/UniGetUI/choco-cli/extensions/chocolatey-visualstudio/
|
||||
src/UniGetUI/choco-cli/extensions/chocolatey-dotnetfx/
|
||||
|
||||
*.user
|
||||
/src/UniGetUI/Generated Files
|
||||
/src/UniGetUI.Avalonia/Infrastructure/Generated Files
|
||||
/InstallerExtras/MsiCreator/.vs/MsiInstallerWrapper/CopilotIndices
|
||||
/InstallerExtras/MsiCreator/.vs
|
||||
InstallerExtras/MsiCreator/setup.exe
|
||||
InstallerExtras/MsiCreator/UniGetUI Installer.msi
|
||||
InstallerExtras/MsiCreator/UniGetUISetup.msi
|
||||
UniGetUI.Installer.ms-store-test.exe
|
||||
UniGetUI Installer_winget-fix-test.exe
|
||||
InstallerExtras/uninst-*.e32
|
||||
src/.idea/
|
||||
src/UniGetUI.v3.ncrunchsolution
|
||||
|
||||
# macOS Finder metadata
|
||||
.DS_Store
|
||||
/src/UniGetUI.Avalonia/Generated Files
|
||||
@@ -0,0 +1,108 @@
|
||||
# UniGetUI - Copilot Instructions
|
||||
|
||||
## Project Overview
|
||||
|
||||
UniGetUI is an Avalonia desktop app (C#/.NET 10) providing a GUI for CLI package managers (WinGet, Scoop, Chocolatey, Pip, Npm, .NET Tool, PowerShell Gallery, Cargo, Vcpkg).
|
||||
|
||||
Solution entry points:
|
||||
- `src/UniGetUI.Windows.slnx` - official Windows solution; builds the Avalonia app and Windows-specific package-manager integrations
|
||||
- `src/UniGetUI.Avalonia.slnx` - cross-platform Avalonia solution
|
||||
|
||||
## Architecture
|
||||
|
||||
The codebase follows a **layered, modular structure** with ~40 projects:
|
||||
|
||||
- **`UniGetUI.Avalonia/`** - Avalonia entry point, AXAML pages, controls, and app shell (`Program.cs`, `Views/MainWindow.axaml`)
|
||||
- **`SharedAssets/`** - shared app icons, symbols, splash images, and utility scripts used by packaging
|
||||
- **`UniGetUI.Core.*`** - Shared infrastructure: `Logger`, `Settings`, `Tools` (includes `CoreTools.Translate()`), `IconEngine`, `LanguageEngine`
|
||||
- **`UniGetUI.PackageEngine.Interfaces`** - Contracts: `IPackageManager`, `IPackage`, `IManagerSource`, `IPackageDetails`
|
||||
- **`UniGetUI.PackageEngine.PackageManagerClasses`** - Base implementations: `PackageManager` (abstract), `Package`, helpers (`BasePkgDetailsHelper`, `BasePkgOperationHelper`, `BaseSourceHelper`)
|
||||
- **`UniGetUI.PackageEngine.Managers.*`** - Concrete manager implementations (one project per manager: `WinGet`, `Scoop`, `Chocolatey`, `Pip`, `npm`, etc.)
|
||||
- **`UniGetUI.PackageEngine.Operations`** - Install/update/uninstall operation orchestration
|
||||
- **`UniGetUI.Interface.*`** - Enums, telemetry, background API
|
||||
|
||||
## Adding a New Package Manager
|
||||
|
||||
Each manager extends `PackageManager` and must override three abstract methods:
|
||||
|
||||
```csharp
|
||||
protected override IReadOnlyList<Package> FindPackages_UnSafe(string query);
|
||||
protected override IReadOnlyList<Package> GetAvailableUpdates_UnSafe();
|
||||
protected override IReadOnlyList<Package> GetInstalledPackages_UnSafe();
|
||||
```
|
||||
|
||||
Each manager also provides three helper classes (in a `Helpers/` subfolder):
|
||||
- `*PkgDetailsHelper` extends `BasePkgDetailsHelper` - overrides `GetDetails_UnSafe`, `GetInstallableVersions_UnSafe`, `GetIcon_UnSafe`, etc.
|
||||
- `*PkgOperationHelper` extends `BasePkgOperationHelper` - overrides `_getOperationParameters`, `_getOperationResult`
|
||||
- `*SourceHelper` extends `BaseSourceHelper` - overrides `GetSources_UnSafe`, `GetAddSourceParameters`, etc.
|
||||
|
||||
The constructor sets `Capabilities`, `Properties`, and wires the helpers. See `src/UniGetUI.PackageEngine.Managers.Scoop/Scoop.cs` as a clean reference implementation.
|
||||
|
||||
## Build & Test
|
||||
|
||||
```shell
|
||||
# Restore & test (from src/)
|
||||
dotnet restore UniGetUI.Windows.slnx
|
||||
dotnet test UniGetUI.Windows.slnx --verbosity q --nologo /p:Platform=x64
|
||||
|
||||
# Publish release build
|
||||
dotnet publish src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj /p:Configuration=Release /p:Platform=x64 -p:RuntimeIdentifier=win-x64
|
||||
```
|
||||
|
||||
- Target framework: `net10.0-windows10.0.26100.0` (min `10.0.19041`)
|
||||
- Build generates secrets via `src/UniGetUI.Avalonia/Infrastructure/generate-secrets.ps1` and integrity tree via `scripts/generate-integrity-tree.ps1`
|
||||
- Self-contained, publish-trimmed (partial), Windows App SDK self-contained
|
||||
- Tests use **xUnit** (`[Fact]`, `Assert.*`)
|
||||
|
||||
## Avalonia DevTools (Developer-Only)
|
||||
|
||||
Use these rules when changing Avalonia diagnostics/devtools behavior:
|
||||
|
||||
- Build-time switch is `EnableAvaloniaDiagnostics` in `src/Directory.Build.props`.
|
||||
- Default policy: enabled in `Debug`, disabled in `Release`.
|
||||
- `src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj` must condition `AvaloniaUI.DiagnosticsSupport` on `$(EnableAvaloniaDiagnostics)`.
|
||||
- Compile-time diagnostics code in `src/UniGetUI.Avalonia/Program.cs` must be gated by `#if AVALONIA_DIAGNOSTICS_ENABLED` (not `#if DEBUG`).
|
||||
- Runtime controls are developer-only and intentionally not listed in `docs/CLI.md`.
|
||||
- Runtime precedence in `Program.cs`: CLI flags > `UNIGETUI_AVALONIA_DEVTOOLS` environment variable > `Auto` default.
|
||||
- Accepted runtime env/CLI values for mode parsing: `auto`, `enabled`, `disabled`, `on`, `off`, `true`, `false`, `1`, `0`.
|
||||
- `Auto` mode must remain WSL-safe (DevTools disabled by default on WSL).
|
||||
- If diagnostics were excluded at build time, runtime toggle requests should log a no-op warning.
|
||||
|
||||
## Key Patterns & Conventions
|
||||
|
||||
### Settings
|
||||
File-based settings via `Settings.Get(Settings.K.*)` / `Settings.Set(Settings.K.*, value)` and `Settings.GetValue(Settings.K.*)` / `Settings.SetValue(Settings.K.*, value)`. Setting keys are defined in the `Settings.K` enum in `SettingsEngine_Names.cs`. Boolean settings are stored as file existence; string settings as file content.
|
||||
|
||||
### Logging
|
||||
Use `Logger.Info()`, `Logger.Warn()`, `Logger.Error()`, `Logger.Debug()`, `Logger.ImportantInfo()` from `UniGetUI.Core.Logging`. Accepts both `string` and `Exception` parameters.
|
||||
|
||||
### Localization
|
||||
Use `CoreTools.Translate("text")` for all user-facing strings. Parameterized: `CoreTools.Translate("{0} packages found", count)`. In XAML, use the `TranslatedTextBlock` control. Translation assets live under `src/Languages/`; do not assume Tolgee-based automation exists in this repository.
|
||||
|
||||
### Naming
|
||||
- Types, methods, properties: **PascalCase**
|
||||
- Private fields: `__doubleUnderscore` or `_singleUnderscore` prefix
|
||||
- Internal unsafe methods: suffix `_UnSafe` (e.g., `FindPackages_UnSafe`)
|
||||
- Nullable enabled globally; `LangVersion` is `latest`
|
||||
- Code style enforced in build (`EnforceCodeStyleInBuild=true`)
|
||||
|
||||
### Manager conventions
|
||||
- `FALSE_PACKAGE_NAMES`, `FALSE_PACKAGE_IDS`, `FALSE_PACKAGE_VERSIONS` static arrays filter CLI parsing noise
|
||||
- Manager initialization flows through `Initialize()` -> `_loadManagerExecutableFile()` -> `_loadManagerVersion()` -> `_performExtraLoadingSteps()`
|
||||
- Operations that may fail return `OperationVeredict` (note: intentional misspelling used throughout codebase)
|
||||
|
||||
## Key Files
|
||||
|
||||
| Purpose | Path |
|
||||
|---|---|
|
||||
| Windows solution | `src/UniGetUI.Windows.slnx` |
|
||||
| Cross-platform solution | `src/UniGetUI.Avalonia.slnx` |
|
||||
| Shared build props | `src/Directory.Build.props` |
|
||||
| Version info | `src/SharedAssemblyInfo.cs` |
|
||||
| Manager interface | `src/UniGetUI.PackageEngine.Interfaces/IPackageManager.cs` |
|
||||
| Base manager class | `src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs` |
|
||||
| Package class | `src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/Package.cs` |
|
||||
| Settings engine | `src/UniGetUI.Core.Settings/SettingsEngine.cs` |
|
||||
| Setting keys | `src/UniGetUI.Core.Settings/SettingsEngine_Names.cs` |
|
||||
| Logger | `src/UniGetUI.Core.Logger/Logger.cs` |
|
||||
| CI test workflow | `.github/workflows/dotnet-test.yml` |
|
||||
@@ -0,0 +1,13 @@
|
||||
# Code of Conduct
|
||||
|
||||
UniGetUI follows the [Contributor Covenant Code of Conduct, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
|
||||
|
||||
## Providing feedback
|
||||
|
||||
We are happy to receive constructive feedback, including criticism, and we value the time people take to help improve UniGetUI. The most helpful feedback is specific, respectful, and actionable: describe what changed, where it happens, what you expected instead, and any details that would help reproduce or evaluate the issue.
|
||||
|
||||
Feedback about design, performance, usability, or project direction is welcome, especially when it focuses on concrete observations and possible improvements. Personal attacks, insults, repeated off-topic comments, pile-ons across unrelated issues or pull requests, and broad demands that cannot be acted on do not help the project and may be moderated.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported privately through the [Devolutions contact page](https://devolutions.net/contact/).
|
||||
|
||||
Security vulnerabilities should be reported through the [Devolutions security page](https://devolutions.net/security/).
|
||||
@@ -0,0 +1,64 @@
|
||||
# Contributing to UniGetUI
|
||||
|
||||
Thank you for helping improve UniGetUI. Please follow the [Code of Conduct](CODE_OF_CONDUCT.md) in all project spaces.
|
||||
|
||||
## Issues and feature requests
|
||||
|
||||
Use the GitHub issue forms and choose the template that best matches your request:
|
||||
|
||||
- **Bug or issue:** use the bug template, include the UniGetUI version, platform details, logs, and reproduction steps when available.
|
||||
- **Hard crash:** use the hard-crash template.
|
||||
- **New feature:** use the feature request template.
|
||||
- **Improvement to an existing feature:** use the enhancement/improvement template.
|
||||
|
||||
Before opening an issue, search for duplicates and check whether the problem is specific to UniGetUI. If the same behavior can be reproduced directly with the underlying package manager or package, report it to that project first.
|
||||
|
||||
Do not include secrets, tokens, personal data, or private logs in public issues. Security vulnerabilities should be reported through the [Devolutions security page](https://devolutions.net/security/). Private non-security inquiries can be sent through the [Devolutions contact page](https://devolutions.net/contact/).
|
||||
|
||||
Feature requests and improvements are reviewed by the maintainers and prioritized based on scope, impact, maintainability, and the project roadmap.
|
||||
|
||||
## Pull requests
|
||||
|
||||
- Use the pull request template and describe the user-visible change.
|
||||
- Keep pull requests focused on one feature, bug fix, or documentation update.
|
||||
- Link related issues when applicable, but do not create placeholder issues just to justify a pull request.
|
||||
- Mark unfinished work as a draft pull request.
|
||||
- Make sure the project builds and the affected behavior is tested before requesting review.
|
||||
- Spam, unrelated, non-building, or low-effort pull requests may be closed without review.
|
||||
|
||||
## Development setup and validation
|
||||
|
||||
Run the pre-commit hook setup once after cloning:
|
||||
|
||||
```powershell
|
||||
pwsh ./scripts/install-git-hooks.ps1
|
||||
```
|
||||
|
||||
The hook runs whitespace formatting on staged files under `src` when the `dotnet` CLI is available. If it rewrites files, review the changes and commit again.
|
||||
|
||||
Useful local validation commands from the repository root:
|
||||
|
||||
```powershell
|
||||
dotnet format whitespace src --folder --verify-no-changes
|
||||
dotnet restore src/UniGetUI.Windows.slnx
|
||||
dotnet format style src/UniGetUI.Windows.slnx --no-restore --verify-no-changes
|
||||
dotnet test src/UniGetUI.Windows.slnx --no-restore --verbosity q --nologo /p:Platform=x64
|
||||
```
|
||||
|
||||
## Coding guidelines
|
||||
|
||||
UniGetUI is primarily a C#/.NET Avalonia application. Follow the existing codebase style and patterns:
|
||||
|
||||
- Use PascalCase for types, methods, and properties.
|
||||
- Follow existing private field conventions, including `_singleUnderscore` and `__doubleUnderscore` prefixes.
|
||||
- Keep nullable reference types and type safety intact; avoid unnecessary casts and broad catch blocks.
|
||||
- Use the `_UnSafe` suffix for internal unsafe package-manager methods when matching existing package-engine patterns.
|
||||
- Localize user-facing strings with `CoreTools.Translate(...)`.
|
||||
- Use the existing logging APIs from `UniGetUI.Core.Logging`.
|
||||
- Keep changes focused and avoid unrelated refactors.
|
||||
|
||||
## Commits
|
||||
|
||||
- Keep each commit focused on a single logical change.
|
||||
- Do not leave the project in a broken or non-buildable state between commits.
|
||||
- Use clear commit messages and reference related issues when applicable.
|
||||
@@ -0,0 +1,314 @@
|
||||
; Modified from https://github.com/DomGries/InnoDependencyInstaller
|
||||
|
||||
|
||||
|
||||
[Code]
|
||||
// types and variables
|
||||
type
|
||||
TDependency_Entry = record
|
||||
Filename: String;
|
||||
Parameters: String;
|
||||
Title: String;
|
||||
URL: String;
|
||||
Checksum: String;
|
||||
ForceSuccess: Boolean;
|
||||
RestartAfter: Boolean;
|
||||
end;
|
||||
|
||||
var
|
||||
Dependency_Memo: String;
|
||||
Dependency_List: array of TDependency_Entry;
|
||||
Dependency_NeedRestart, Dependency_ForceX86: Boolean;
|
||||
Dependency_DownloadPage: TDownloadWizardPage;
|
||||
|
||||
procedure Dependency_Add(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess, RestartAfter: Boolean);
|
||||
var
|
||||
Dependency: TDependency_Entry;
|
||||
DependencyCount: Integer;
|
||||
begin
|
||||
Dependency_Memo := Dependency_Memo + #13#10 + '%1' + Title;
|
||||
|
||||
Dependency.Filename := Filename;
|
||||
Dependency.Parameters := Parameters;
|
||||
Dependency.Title := Title;
|
||||
|
||||
if FileExists(ExpandConstant('{tmp}{\}') + Filename) then begin
|
||||
Dependency.URL := '';
|
||||
end else begin
|
||||
Dependency.URL := URL;
|
||||
end;
|
||||
|
||||
Dependency.Checksum := Checksum;
|
||||
Dependency.ForceSuccess := ForceSuccess;
|
||||
Dependency.RestartAfter := RestartAfter;
|
||||
|
||||
DependencyCount := GetArrayLength(Dependency_List);
|
||||
SetArrayLength(Dependency_List, DependencyCount + 1);
|
||||
Dependency_List[DependencyCount] := Dependency;
|
||||
end;
|
||||
|
||||
<event('InitializeWizard')>
|
||||
procedure Dependency_Internal1;
|
||||
begin
|
||||
Dependency_DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);
|
||||
end;
|
||||
|
||||
<event('PrepareToInstall')>
|
||||
function Dependency_Internal2(var NeedsRestart: Boolean): String;
|
||||
var
|
||||
DependencyCount, DependencyIndex, ResultCode: Integer;
|
||||
Retry: Boolean;
|
||||
TempValue: String;
|
||||
begin
|
||||
DependencyCount := GetArrayLength(Dependency_List);
|
||||
|
||||
if DependencyCount > 0 then begin
|
||||
Dependency_DownloadPage.Show;
|
||||
|
||||
for DependencyIndex := 0 to DependencyCount - 1 do begin
|
||||
if Dependency_List[DependencyIndex].URL <> '' then begin
|
||||
Dependency_DownloadPage.Clear;
|
||||
Dependency_DownloadPage.Add(Dependency_List[DependencyIndex].URL, Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Checksum);
|
||||
|
||||
Retry := True;
|
||||
while Retry do begin
|
||||
Retry := False;
|
||||
|
||||
try
|
||||
Dependency_DownloadPage.Download;
|
||||
except
|
||||
if Dependency_DownloadPage.AbortedByUser then begin
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
DependencyIndex := DependencyCount;
|
||||
end else begin
|
||||
case SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of
|
||||
IDABORT: begin
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
DependencyIndex := DependencyCount;
|
||||
end;
|
||||
IDRETRY: begin
|
||||
Retry := True;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
if Result = '' then begin
|
||||
for DependencyIndex := 0 to DependencyCount - 1 do begin
|
||||
Dependency_DownloadPage.SetText(Dependency_List[DependencyIndex].Title, '');
|
||||
Dependency_DownloadPage.SetProgress(DependencyIndex + 1, DependencyCount + 1);
|
||||
|
||||
while True do begin
|
||||
ResultCode := 0;
|
||||
if ShellExec('', ExpandConstant('{tmp}{\}') + Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then begin
|
||||
if Dependency_List[DependencyIndex].RestartAfter then begin
|
||||
if DependencyIndex = DependencyCount - 1 then begin
|
||||
Dependency_NeedRestart := True;
|
||||
end else begin
|
||||
NeedsRestart := True;
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
end;
|
||||
break;
|
||||
end else if (ResultCode = 0) or Dependency_List[DependencyIndex].ForceSuccess then begin // ERROR_SUCCESS (0)
|
||||
break;
|
||||
end else if ResultCode = 1641 then begin // ERROR_SUCCESS_REBOOT_INITIATED (1641)
|
||||
NeedsRestart := True;
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
break;
|
||||
end else if ResultCode = 3010 then begin // ERROR_SUCCESS_REBOOT_REQUIRED (3010)
|
||||
Dependency_NeedRestart := True;
|
||||
break;
|
||||
end;
|
||||
end;
|
||||
|
||||
case SuppressibleMsgBox(FmtMessage(SetupMessage(msgErrorFunctionFailed), [Dependency_List[DependencyIndex].Title, IntToStr(ResultCode)]), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of
|
||||
IDABORT: begin
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
break;
|
||||
end;
|
||||
IDIGNORE: begin
|
||||
break;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
if Result <> '' then begin
|
||||
break;
|
||||
end;
|
||||
end;
|
||||
|
||||
if NeedsRestart then begin
|
||||
TempValue := '"' + ExpandConstant('{srcexe}') + '" /restart=1 /LANG="' + ExpandConstant('{language}') + '" /DIR="' + WizardDirValue + '" /GROUP="' + WizardGroupValue + '" /TYPE="' + WizardSetupType(False) + '" /COMPONENTS="' + WizardSelectedComponents(False) + '" /TASKS="' + WizardSelectedTasks(False) + '"';
|
||||
if WizardNoIcons then begin
|
||||
TempValue := TempValue + ' /NOICONS';
|
||||
end;
|
||||
RegWriteStringValue(HKA, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', '{#SetupSetting("AppName")}', TempValue);
|
||||
end;
|
||||
end;
|
||||
|
||||
Dependency_DownloadPage.Hide;
|
||||
end;
|
||||
end;
|
||||
|
||||
<event('UpdateReadyMemo')>
|
||||
function Dependency_Internal3(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String;
|
||||
begin
|
||||
Result := '';
|
||||
if MemoUserInfoInfo <> '' then begin
|
||||
Result := Result + MemoUserInfoInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoDirInfo <> '' then begin
|
||||
Result := Result + MemoDirInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoTypeInfo <> '' then begin
|
||||
Result := Result + MemoTypeInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoComponentsInfo <> '' then begin
|
||||
Result := Result + MemoComponentsInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoGroupInfo <> '' then begin
|
||||
Result := Result + MemoGroupInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoTasksInfo <> '' then begin
|
||||
Result := Result + MemoTasksInfo;
|
||||
end;
|
||||
|
||||
if Dependency_Memo <> '' then begin
|
||||
if MemoTasksInfo = '' then begin
|
||||
Result := Result + SetupMessage(msgReadyMemoTasks);
|
||||
end;
|
||||
Result := Result + FmtMessage(Dependency_Memo, [Space]);
|
||||
end;
|
||||
end;
|
||||
|
||||
<event('NeedRestart')>
|
||||
function Dependency_Internal4: Boolean;
|
||||
begin
|
||||
Result := Dependency_NeedRestart;
|
||||
end;
|
||||
|
||||
function Dependency_IsX64: Boolean;
|
||||
begin
|
||||
Result := not Dependency_ForceX86 and Is64BitInstallMode;
|
||||
end;
|
||||
|
||||
function Dependency_String(const x86, x64: String): String;
|
||||
begin
|
||||
if Dependency_IsX64 then begin
|
||||
Result := x64;
|
||||
end else begin
|
||||
Result := x86;
|
||||
end;
|
||||
end;
|
||||
|
||||
function Dependency_ArchSuffix: String;
|
||||
begin
|
||||
Result := Dependency_String('_x64', '_x64');
|
||||
end;
|
||||
|
||||
function Dependency_ArchTitle: String;
|
||||
begin
|
||||
Result := Dependency_String(' (x86)', ' (x64)');
|
||||
end;
|
||||
|
||||
function Dependency_IsNetCoreInstalled(const Version: String): Boolean;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
// source code: https://github.com/dotnet/deployment-tools/tree/main/src/clickonce/native/projects/NetCoreCheck
|
||||
if not FileExists(ExpandConstant('{tmp}{\}') + 'netcorecheck' + Dependency_ArchSuffix + '.exe') then begin
|
||||
ExtractTemporaryFile('netcorecheck' + Dependency_ArchSuffix + '.exe');
|
||||
end;
|
||||
Result := ShellExec('', ExpandConstant('{tmp}{\}') + 'netcorecheck' + Dependency_ArchSuffix + '.exe', Version, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0);
|
||||
end;
|
||||
|
||||
function Dependency_IsVCRuntimeInstalled(const Arch: String; const Major, Minor, Bld: Cardinal): Boolean;
|
||||
var
|
||||
InstalledFlag, InstMajor, InstMinor, InstBld: Cardinal;
|
||||
Key: String;
|
||||
begin
|
||||
// Canonical VC++ runtime detection per Microsoft docs:
|
||||
// https://learn.microsoft.com/en-us/cpp/windows/redistributing-visual-cpp-files
|
||||
// The redistributable installer writes these values to both registry views,
|
||||
// so a plain HKLM read is correct from Inno Setup's 32-bit process.
|
||||
Key := 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\' + Arch;
|
||||
Result :=
|
||||
RegQueryDWordValue(HKLM, Key, 'Installed', InstalledFlag) and (InstalledFlag = 1) and
|
||||
RegQueryDWordValue(HKLM, Key, 'Major', InstMajor) and
|
||||
RegQueryDWordValue(HKLM, Key, 'Minor', InstMinor) and
|
||||
RegQueryDWordValue(HKLM, Key, 'Bld', InstBld) and
|
||||
((InstMajor > Major) or
|
||||
((InstMajor = Major) and (InstMinor > Minor)) or
|
||||
((InstMajor = Major) and (InstMinor = Minor) and (InstBld >= Bld)));
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2015To2022;
|
||||
var
|
||||
Arch, Url: String;
|
||||
MinMajor, MinMinor, MinBld: Cardinal;
|
||||
begin
|
||||
// https://docs.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist
|
||||
MinMajor := 14;
|
||||
MinMinor := 30;
|
||||
MinBld := 30704;
|
||||
|
||||
if IsARM64 then begin
|
||||
Arch := 'arm64';
|
||||
Url := 'https://aka.ms/vc14/vc_redist.arm64.exe';
|
||||
end else begin
|
||||
Arch := 'x64';
|
||||
Url := 'https://aka.ms/vc14/vc_redist.x64.exe';
|
||||
end;
|
||||
|
||||
// Primary: registry-based detection (Microsoft's documented method, stable
|
||||
// across installer builds). Fallback: MSI UpgradeCode lookup for x64 only
|
||||
// (the x64 UpgradeCode {36F68A90-...} is verified; keeping a fallback helps
|
||||
// on unusual install states). See issue #4596 for the regression this
|
||||
// replaces, where a ProductCode was mistakenly used with IsMsiProductInstalled.
|
||||
if Dependency_IsVCRuntimeInstalled(Arch, MinMajor, MinMinor, MinBld) then Exit;
|
||||
if (Arch = 'x64') and IsMsiProductInstalled('{36F68A90-239C-34DF-B58C-64B30153CE35}',
|
||||
PackVersionComponents(MinMajor, MinMinor, MinBld, 0)) then Exit;
|
||||
|
||||
Dependency_Add('vcredist2022_' + Arch + '.exe',
|
||||
'/passive /norestart',
|
||||
'Visual C++ 2015-2022 Redistributable (' + Arch + ')',
|
||||
Url,
|
||||
'', False, False);
|
||||
end;
|
||||
|
||||
|
||||
procedure Dependency_AddWebView2;
|
||||
var
|
||||
WebView2URL, WebView2Title: String;
|
||||
begin
|
||||
// https://developer.microsoft.com/en-us/microsoft-edge/webview2
|
||||
if not RegValueExists(HKLM, Dependency_String('SOFTWARE', 'SOFTWARE\WOW6432Node') + '\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv') then begin
|
||||
if IsARM64 then begin
|
||||
WebView2URL := 'https://go.microsoft.com/fwlink/?linkid=2099616';
|
||||
WebView2Title := 'WebView2 Runtime (ARM64)';
|
||||
end else begin
|
||||
WebView2URL := 'https://go.microsoft.com/fwlink/?linkid=2124701';
|
||||
WebView2Title := 'WebView2 Runtime (x64)';
|
||||
end;
|
||||
Dependency_Add('MicrosoftEdgeWebview2Setup.exe',
|
||||
'/silent /install',
|
||||
WebView2Title,
|
||||
WebView2URL,
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
|
||||
[Files]
|
||||
#ifdef Dependency_Path_NetCoreCheck
|
||||
; download netcorecheck_x64.exe: https://www.nuget.org/packages/Microsoft.NET.Tools.NETCoreCheck.x64
|
||||
Source: "{#Dependency_Path_NetCoreCheck}netcorecheck_x64.exe"; Flags: dontcopy noencryption
|
||||
#endif
|
||||
|
||||
#ifdef Dependency_Path_DirectX
|
||||
Source: "{#Dependency_Path_DirectX}dxwebsetup.exe"; Flags: dontcopy noencryption
|
||||
#endif
|
||||
@@ -0,0 +1,218 @@
|
||||
[CustomMessages]
|
||||
; Armenian, Brazilian Portuguese, Catalan, Corsican, Czech, Danish, Dutch, Finnish, French, German, Hebrew, Icelandic, Italian, Japanese, Korean, Norwegian, Polish, Portuguese, Russian, Slovenian, Spanish, Turkish, Ukrainian
|
||||
|
||||
; English
|
||||
InstallType=Installation type
|
||||
ShCuts=Shortcuts
|
||||
PortInst=Perform a portable installation
|
||||
RegInst=Perform a regular installation
|
||||
RegStartMmenuIcon=Create a shortcut on the Start menu
|
||||
RegDesktopIcon=Create a shortcut on the Desktop
|
||||
PackageBundleName=UniGetUI package bundle
|
||||
|
||||
; Armenian
|
||||
Armenian.InstallType=Տեղադրման տեսակը
|
||||
Armenian.ShCuts=Դյուրանցումներ
|
||||
Armenian.PortInst=Կատարել շարժական տեղադրում
|
||||
Armenian.RegInst=Կատարել սովորական տեղադրում
|
||||
Armenian.RegStartMmenuIcon=Ստեղծեք դյուրանցում Start ընտրացանկում
|
||||
Armenian.RegDesktopIcon=Ստեղծեք դյուրանցում աշխատասեղանի վրա
|
||||
Armenian.PackageBundleName=UniGetUI փաթեթի փաթեթ
|
||||
|
||||
; BrazilianPortuguese
|
||||
BrazilianPortuguese.InstallType=Tipo de instalação
|
||||
BrazilianPortuguese.ShCuts=Atalhos
|
||||
BrazilianPortuguese.PortInst=Realizar uma instalação portátil
|
||||
BrazilianPortuguese.RegInst=Realizar uma instalação regular
|
||||
BrazilianPortuguese.RegStartMmenuIcon=Criar um atalho no menu Iniciar
|
||||
BrazilianPortuguese.RegDesktopIcon=Criar um atalho na área de trabalh
|
||||
BrazilianPortuguese.PackageBundleName=Coleção de pacotes UniGetUI
|
||||
|
||||
; Catalan
|
||||
Catalan.InstallType=Tipus d'instal·lació
|
||||
Catalan.ShCuts=Dreceres
|
||||
Catalan.PortInst=Feu una instal·lació portàtil
|
||||
Catalan.RegInst=Feu una instal·lació regular
|
||||
Catalan.RegStartMmenuIcon=Creeu una drecera al menú Inici
|
||||
Catalan.RegDesktopIcon=Creeu una drecera a l'Escriptori
|
||||
Catalan.PackageBundleName=Col·lecció de paquets de l'UniGetUI
|
||||
|
||||
; Corsican
|
||||
Corsican.InstallType=Tipu d'installazione
|
||||
Corsican.ShCuts=Scorciatoie
|
||||
Corsican.PortInst=Eseguite una installazione portable
|
||||
Corsican.RegInst=Eseguite una installazione regulare
|
||||
Corsican.RegStartMmenuIcon=Create una scorciatoia nant'à u menu Start
|
||||
Corsican.RegDesktopIcon=Create una scorciatoia nant'à u Desktop
|
||||
Corsican.PackageBundleName=Raccolta di pacchetti UniGetUI
|
||||
|
||||
; Czech
|
||||
Czech.InstallType=Typ instalace
|
||||
Czech.ShCuts=Zkratky
|
||||
Czech.PortInst=Proveďte přenosnou instalaci
|
||||
Czech.RegInst=Proveďte běžnou instalaci
|
||||
Czech.RegStartMmenuIcon=Vytvořte zástupce v nabídce Start
|
||||
Czech.RegDesktopIcon=Vytvořte zástupce na ploše
|
||||
Czech.PackageBundleName=Kolekce balíčků UniGetUI
|
||||
|
||||
; Danish
|
||||
Danish.InstallType=Installationstype
|
||||
Danish.ShCuts=Genveje
|
||||
Danish.PortInst=Udfør en bærbar installation
|
||||
Danish.RegInst=Udfør en almindelig installation
|
||||
Danish.RegStartMmenuIcon=Opret en genvej på Start-menuen
|
||||
Danish.RegDesktopIcon=Opret en genvej på skrivebordet
|
||||
Danish.PackageBundleName=Samling af UniGetUI-pakker
|
||||
|
||||
; Dutch
|
||||
Dutch.InstallType=Installatietype
|
||||
Dutch.ShCuts=Snelkoppelingen
|
||||
Dutch.PortInst=Voer een draagbare installatie uit
|
||||
Dutch.RegInst=Voer een normale installatie uit
|
||||
Dutch.RegStartMmenuIcon=Maak een snelkoppeling in het Startmenu
|
||||
Dutch.RegDesktopIcon=Maak een snelkoppeling op het bureaublad
|
||||
Dutch.PackageBundleName=Verzameling UniGetUI-pakketten
|
||||
|
||||
; Finnish
|
||||
Finnish.InstallType=Asennustyyppi
|
||||
Finnish.ShCuts=Oikopolut
|
||||
Finnish.PortInst=Suorita kannettava asennus
|
||||
Finnish.RegInst=Suorita tavallinen asennus
|
||||
Finnish.RegStartMmenuIcon=Luo pikakuvake Käynnistä-valikkoon
|
||||
Finnish.RegDesktopIcon=Luo pikakuvake työpöydälle
|
||||
Finnish.PackageBundleName=Kokoelma UniGetUI-paketteja
|
||||
|
||||
; French
|
||||
French.InstallType=Type d'installation
|
||||
French.ShCuts=Raccourcis
|
||||
French.PortInst=Effectuer une installation portable
|
||||
French.RegInst=Effectuer une installation normale
|
||||
French.RegStartMmenuIcon=Créer un raccourci dans le menu Démarrer
|
||||
French.RegDesktopIcon=Créer un raccourci sur le bureau
|
||||
French.PackageBundleName=Collection de packages UniGetUI
|
||||
|
||||
; German
|
||||
German.InstallType=Installationstyp
|
||||
German.ShCuts=Verknüpfungen
|
||||
German.PortInst=Führe eine portable Installation durch
|
||||
German.RegInst=Führe eine normale Installation durch
|
||||
German.RegStartMmenuIcon=Erstelle eine Verknüpfung im Startmenü
|
||||
German.RegDesktopIcon=Erstelle eine Verknüpfung auf dem Desktop
|
||||
German.PackageBundleName=Sammlung von UniGetUI-Paketen
|
||||
|
||||
; Hebrew
|
||||
Hebrew.InstallType=סוג התקנה
|
||||
Hebrew.ShCuts=קיצורי דרך
|
||||
Hebrew.PortInst=בצע התקנה ניידת
|
||||
Hebrew.RegInst=בצע התקנה רגילה
|
||||
Hebrew.RegStartMmenuIcon=צור קיצור דרך בתפריט התחל
|
||||
Hebrew.RegDesktopIcon=צור קיצור דרך בשולחן העבודה
|
||||
Hebrew.PackageBundleName=אוסף חבילות UniGetUI
|
||||
|
||||
; Icelandic
|
||||
;Icelandic.InstallType=Uppsetningargerð
|
||||
;Icelandic.ShCuts=Flýtivísar
|
||||
;Icelandic.PortInst=Framkvæma færanlega uppsetningu
|
||||
;Icelandic.RegInst=Framkvæma reglulega uppsetningu
|
||||
;Icelandic.RegStartMmenuIcon=Búa til flýtileið á Start valmyndinni
|
||||
;Icelandic.RegDesktopIcon=Búa til flýtileið á skjáborðinu
|
||||
;Icelandic.PackageBundleName=Safn af UniGetUI pakka
|
||||
|
||||
; Italian
|
||||
Italian.InstallType=Tipo di installazione
|
||||
Italian.ShCuts=Collegamenti
|
||||
Italian.PortInst=Esegui installazione portatile
|
||||
Italian.RegInst=Esegui installazione normale
|
||||
Italian.RegStartMmenuIcon=Crea una scorciatoia nel menu Start
|
||||
Italian.RegDesktopIcon=Crea una scorciatoia sul Desktop
|
||||
Italian.PackageBundleName=Raccolta di pacchetti UniGetUI
|
||||
|
||||
; Japanese
|
||||
Japanese.InstallType=インストールの種類
|
||||
Japanese.ShCuts=ショートカット
|
||||
Japanese.PortInst=ポータブル インストールを実行する
|
||||
Japanese.RegInst=通常のインストールを実行します
|
||||
Japanese.RegStartMmenuIcon=スタート メニューにショートカットを作成する
|
||||
Japanese.RegDesktopIcon=デスクトップにショートカットを作成する
|
||||
Japanese.PackageBundleName=UniGetUI パッケージのコレクション
|
||||
|
||||
; Korean
|
||||
Korean.InstallType=설치 유형
|
||||
Korean.ShCuts=바로가기
|
||||
Korean.PortInst=이동식 설치 수행
|
||||
Korean.RegInst=일반 설치 수행
|
||||
Korean.RegStartMmenuIcon=시작 메뉴에 바로가기 만들기
|
||||
Korean.RegDesktopIcon=바탕화면에 바로가기 생성
|
||||
Korean.PackageBundleName=UniGetUI 패키지 컬렉션
|
||||
|
||||
; Norwegian
|
||||
Norwegian.InstallType=Installasjonstype
|
||||
Norwegian.ShCuts=Snarveier
|
||||
Norwegian.PortInst=Utfør en bærbar installasjon
|
||||
Norwegian.RegInst=Utfør en vanlig installasjon
|
||||
Norwegian.RegStartMmenuIcon=Lag en snarvei på Start-menyen
|
||||
Norwegian.RegDesktopIcon=Lag en snarvei på skrivebordet
|
||||
Norwegian.PackageBundleName=Samling av UniGetUI-pakker
|
||||
|
||||
; Polish
|
||||
Polish.InstallType=Typ instalacji
|
||||
Polish.ShCuts=Skróty
|
||||
Polish.PortInst=Przeprowadź instalację przenośną
|
||||
Polish.RegInst=Przeprowadź zwykłą instalację
|
||||
Polish.RegStartMmenuIcon=Utwórz skrót w menu Start
|
||||
Polish.RegDesktopIcon=Utwórz skrót na pulpicie
|
||||
Polish.PackageBundleName=Kolekcja pakietów UniGetUI
|
||||
|
||||
; Portuguese
|
||||
Portuguese.InstallType=Tipo de instalação
|
||||
Portuguese.ShCuts=Atalhos
|
||||
Portuguese.PortInst=Execute uma instalação portátil
|
||||
Portuguese.RegInst=Execute uma instalação regular
|
||||
Portuguese.RegStartMmenuIcon=Criar um atalho no menu Iniciar
|
||||
Portuguese.RegDesktopIcon=Criar um atalho na área de trabalho
|
||||
Portuguese.PackageBundleName=Coleção de pacotes UniGetUI
|
||||
|
||||
; Russian
|
||||
Russian.InstallType=Тип установки
|
||||
Russian.ShCuts=Ярлыки
|
||||
Russian.PortInst=Выполнить переносную установку
|
||||
Russian.RegInst=Выполнить обычную установку
|
||||
Russian.RegStartMmenuIcon=Создать ярлык в меню «Пуск»
|
||||
Russian.RegDesktopIcon=Создать ярлык на рабочем столе
|
||||
Russian.PackageBundleName=
|
||||
|
||||
; Slovenian
|
||||
Slovenian.InstallType=Vrsta namestitve
|
||||
Slovenian.ShCuts=Bližnjice
|
||||
Slovenian.PortInst=Izvedite prenosno namestitev
|
||||
Slovenian.RegInst=Izvedite običajno namestitev
|
||||
Slovenian.RegStartMmenuIcon=Ustvarite bližnjico v meniju Start
|
||||
Slovenian.RegDesktopIcon=Ustvarite bližnjico na namizju
|
||||
Slovenian.PackageBundleName=Коллекция пакетов UniGetUI
|
||||
|
||||
; Spanish
|
||||
Spanish.InstallType=Tipo de instalación
|
||||
Spanish.ShCuts=Atajos
|
||||
Spanish.PortInst=Realizar una instalación portátil
|
||||
Spanish.RegInst=Realizar una instalación regular
|
||||
Spanish.RegStartMmenuIcon=Crear un acceso directo en el menú Inicio
|
||||
Spanish.RegDesktopIcon=Crear un atajo en el escritorio
|
||||
Spanish.PackageBundleName=Coleción de paquetes del UniGetUI
|
||||
|
||||
; Turkish
|
||||
Turkish.InstallType=Yükleme türü
|
||||
Turkish.ShCuts=Kısayollar
|
||||
Turkish.PortInst=Taşınabilir bir kurulum gerçekleştirin
|
||||
Turkish.RegInst=Normal bir kurulum gerçekleştir
|
||||
Turkish.RegStartMmenuIcon=Başlat menüsünde bir kısayol oluştur
|
||||
Turkish.RegDesktopIcon=Masaüstünde bir kısayol oluştur
|
||||
Turkish.PackageBundleName=UniGetUI paketlerinin toplanması
|
||||
|
||||
; Ukrainian
|
||||
Ukrainian.InstallType=Тип інсталяції
|
||||
Ukrainian.ShCuts=Ярлики
|
||||
Ukrainian.PortInst=Виконати переносне встановлення
|
||||
Ukrainian.RegInst=Виконайте звичайну установку
|
||||
Ukrainian.RegStartMmenuIcon=Створити ярлик у меню «Пуск».
|
||||
Ukrainian.RegDesktopIcon=Створити ярлик на робочому столі
|
||||
Ukrainian.PackageBundleName=Колекція пакетів UniGetUI
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.11.35303.130
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "MsiInstallerWrapper", "MsiInstallerWrapper.vdproj", "{EE7E4697-83BE-454A-8188-714D27776721}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Default = Debug|Default
|
||||
Release|Default = Release|Default
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{EE7E4697-83BE-454A-8188-714D27776721}.Debug|Default.ActiveCfg = Debug
|
||||
{EE7E4697-83BE-454A-8188-714D27776721}.Debug|Default.Build.0 = Debug
|
||||
{EE7E4697-83BE-454A-8188-714D27776721}.Release|Default.ActiveCfg = Release
|
||||
{EE7E4697-83BE-454A-8188-714D27776721}.Release|Default.Build.0 = Release
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {FD6F7794-50E5-477C-90BC-D877BB2CDDED}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,712 @@
|
||||
"DeployProject"
|
||||
{
|
||||
"VSVersion" = "3:800"
|
||||
"ProjectType" = "8:{978C614F-708E-4E1A-B201-565925725DBA}"
|
||||
"IsWebType" = "8:FALSE"
|
||||
"ProjectName" = "8:MsiInstallerWrapper"
|
||||
"LanguageId" = "3:1033"
|
||||
"CodePage" = "3:1252"
|
||||
"UILanguageId" = "3:1033"
|
||||
"SccProjectName" = "8:"
|
||||
"SccLocalPath" = "8:"
|
||||
"SccAuxPath" = "8:"
|
||||
"SccProvider" = "8:"
|
||||
"Hierarchy"
|
||||
{
|
||||
"Entry"
|
||||
{
|
||||
"MsmKey" = "8:_A22A7552D1EC4801A0A4D984FCF18331"
|
||||
"OwnerKey" = "8:_UNDEFINED"
|
||||
"MsmSig" = "8:_UNDEFINED"
|
||||
}
|
||||
}
|
||||
"Configurations"
|
||||
{
|
||||
"Debug"
|
||||
{
|
||||
"DisplayName" = "8:Debug"
|
||||
"IsDebugOnly" = "11:TRUE"
|
||||
"IsReleaseOnly" = "11:FALSE"
|
||||
"OutputFilename" = "8:UniGetUISetup.msi"
|
||||
"PackageFilesAs" = "3:2"
|
||||
"PackageFileSize" = "3:-2147483648"
|
||||
"CabType" = "3:1"
|
||||
"Compression" = "3:2"
|
||||
"SignOutput" = "11:FALSE"
|
||||
"CertificateFile" = "8:"
|
||||
"PrivateKeyFile" = "8:"
|
||||
"TimeStampServer" = "8:"
|
||||
"InstallerBootstrapper" = "3:2"
|
||||
"BootstrapperCfg:{63ACBE69-63AA-4F98-B2B6-99F9E24495F2}"
|
||||
{
|
||||
"Enabled" = "11:TRUE"
|
||||
"PromptEnabled" = "11:TRUE"
|
||||
"PrerequisitesLocation" = "2:1"
|
||||
"Url" = "8:"
|
||||
"ComponentsUrl" = "8:"
|
||||
"Items"
|
||||
{
|
||||
"{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:.NETFramework,Version=v4.7.2"
|
||||
{
|
||||
"Name" = "8:Microsoft .NET Framework 4.7.2 (x86 and x64)"
|
||||
"ProductCode" = "8:.NETFramework,Version=v4.7.2"
|
||||
}
|
||||
"{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:Microsoft.EdgeRuntime"
|
||||
{
|
||||
"Name" = "8:Edge WebView runtime"
|
||||
"ProductCode" = "8:Microsoft.EdgeRuntime"
|
||||
}
|
||||
"{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:Microsoft.Visual.C++.14.0.x64"
|
||||
{
|
||||
"Name" = "8:Visual C++ \"14\" Runtime Libraries (x64)"
|
||||
"ProductCode" = "8:Microsoft.Visual.C++.14.0.x64"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"Release"
|
||||
{
|
||||
"DisplayName" = "8:Release"
|
||||
"IsDebugOnly" = "11:FALSE"
|
||||
"IsReleaseOnly" = "11:TRUE"
|
||||
"OutputFilename" = "8:UniGetUISetup.msi"
|
||||
"PackageFilesAs" = "3:2"
|
||||
"PackageFileSize" = "3:-2147483648"
|
||||
"CabType" = "3:1"
|
||||
"Compression" = "3:2"
|
||||
"SignOutput" = "11:FALSE"
|
||||
"CertificateFile" = "8:"
|
||||
"PrivateKeyFile" = "8:"
|
||||
"TimeStampServer" = "8:"
|
||||
"InstallerBootstrapper" = "3:2"
|
||||
"BootstrapperCfg:{63ACBE69-63AA-4F98-B2B6-99F9E24495F2}"
|
||||
{
|
||||
"Enabled" = "11:TRUE"
|
||||
"PromptEnabled" = "11:TRUE"
|
||||
"PrerequisitesLocation" = "2:1"
|
||||
"Url" = "8:"
|
||||
"ComponentsUrl" = "8:"
|
||||
"Items"
|
||||
{
|
||||
"{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:.NETFramework,Version=v4.7.2"
|
||||
{
|
||||
"Name" = "8:Microsoft .NET Framework 4.7.2 (x86 and x64)"
|
||||
"ProductCode" = "8:.NETFramework,Version=v4.7.2"
|
||||
}
|
||||
"{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:Microsoft.EdgeRuntime"
|
||||
{
|
||||
"Name" = "8:Edge WebView runtime"
|
||||
"ProductCode" = "8:Microsoft.EdgeRuntime"
|
||||
}
|
||||
"{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:Microsoft.Visual.C++.14.0.x64"
|
||||
{
|
||||
"Name" = "8:Visual C++ \"14\" Runtime Libraries (x64)"
|
||||
"ProductCode" = "8:Microsoft.Visual.C++.14.0.x64"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"Deployable"
|
||||
{
|
||||
"CustomAction"
|
||||
{
|
||||
"{4AA51A2D-7D85-4A59-BA75-B0809FC8B380}:_BF756C7619CC4B4BBFD0B9E127183C43"
|
||||
{
|
||||
"Name" = "8:%ProgramFiles%\\UniGetUI\\unins000.exe"
|
||||
"Condition" = "8:"
|
||||
"Object" = "8:_A22A7552D1EC4801A0A4D984FCF18331"
|
||||
"FileType" = "3:2"
|
||||
"InstallAction" = "3:4"
|
||||
"Arguments" = "8:"
|
||||
"EntryPoint" = "8:"
|
||||
"Sequence" = "3:1"
|
||||
"Identifier" = "8:_F4F70591_F141_4E92_8171_63B1F09EEAE9"
|
||||
"InstallerClass" = "11:FALSE"
|
||||
"CustomActionData" = "8:"
|
||||
}
|
||||
"{4AA51A2D-7D85-4A59-BA75-B0809FC8B380}:_D68AA58489F8420F9DFCC894A884B2A9"
|
||||
{
|
||||
"Name" = "8:UniGetUI Installer.exe"
|
||||
"Condition" = "8: "
|
||||
"Object" = "8:_A22A7552D1EC4801A0A4D984FCF18331"
|
||||
"FileType" = "3:2"
|
||||
"InstallAction" = "3:1"
|
||||
"Arguments" = "8:/NoAutoStart /ALLUSERS /silent /unattended /NoVCRedist /NoEdgeWebView /NoRestart"
|
||||
"EntryPoint" = "8:"
|
||||
"Sequence" = "3:1"
|
||||
"Identifier" = "8:_AB502465_2086_4B1B_B987_3CD03317278A"
|
||||
"InstallerClass" = "11:FALSE"
|
||||
"CustomActionData" = "8:"
|
||||
"Run64Bit" = "11:FALSE"
|
||||
}
|
||||
}
|
||||
"DefaultFeature"
|
||||
{
|
||||
"Name" = "8:DefaultFeature"
|
||||
"Title" = "8:"
|
||||
"Description" = "8:"
|
||||
}
|
||||
"ExternalPersistence"
|
||||
{
|
||||
"LaunchCondition"
|
||||
{
|
||||
}
|
||||
}
|
||||
"File"
|
||||
{
|
||||
"{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_A22A7552D1EC4801A0A4D984FCF18331"
|
||||
{
|
||||
"SourcePath" = "8:UniGetUI Installer.exe"
|
||||
"TargetName" = "8:UniGetUI Installer.exe"
|
||||
"Tag" = "8:"
|
||||
"Folder" = "8:_B08F8663334547FDBF4D0DC7F957E164"
|
||||
"Condition" = "8:"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Vital" = "11:TRUE"
|
||||
"ReadOnly" = "11:FALSE"
|
||||
"Hidden" = "11:FALSE"
|
||||
"System" = "11:FALSE"
|
||||
"Permanent" = "11:FALSE"
|
||||
"SharedLegacy" = "11:FALSE"
|
||||
"PackageAs" = "3:1"
|
||||
"Register" = "3:1"
|
||||
"Exclude" = "11:FALSE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"IsolateTo" = "8:"
|
||||
}
|
||||
}
|
||||
"FileType"
|
||||
{
|
||||
}
|
||||
"Folder"
|
||||
{
|
||||
"{1525181F-901A-416C-8A58-119130FE478E}:_545FB00BD1854FA8BAF4F057F086F631"
|
||||
{
|
||||
"Name" = "8:#1919"
|
||||
"AlwaysCreate" = "11:FALSE"
|
||||
"Condition" = "8:"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Property" = "8:ProgramMenuFolder"
|
||||
"Folders"
|
||||
{
|
||||
}
|
||||
}
|
||||
"{3C67513D-01DD-4637-8A68-80971EB9504F}:_A6825CAB25F84B15A64AE00EFD72194C"
|
||||
{
|
||||
"DefaultLocation" = "8:[ProgramFiles64Folder][Manufacturer]\\[ProductName]"
|
||||
"Name" = "8:#1925"
|
||||
"AlwaysCreate" = "11:FALSE"
|
||||
"Condition" = "8:"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Property" = "8:TARGETDIR"
|
||||
"Folders"
|
||||
{
|
||||
}
|
||||
}
|
||||
"{1525181F-901A-416C-8A58-119130FE478E}:_AA2E6321A69F4A7F974C661BCDEE307F"
|
||||
{
|
||||
"Name" = "8:#1916"
|
||||
"AlwaysCreate" = "11:FALSE"
|
||||
"Condition" = "8:"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Property" = "8:DesktopFolder"
|
||||
"Folders"
|
||||
{
|
||||
}
|
||||
}
|
||||
"{994432C3-9487-495D-8656-3E829A8DBDDE}:_B08F8663334547FDBF4D0DC7F957E164"
|
||||
{
|
||||
"DefaultLocation" = "8:[WindowsFolder]\\Temp"
|
||||
"Name" = "8:Temp"
|
||||
"AlwaysCreate" = "11:FALSE"
|
||||
"Condition" = "8:"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Property" = "8:NEWPROPERTY1"
|
||||
"Folders"
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
"LaunchCondition"
|
||||
{
|
||||
}
|
||||
"Locator"
|
||||
{
|
||||
}
|
||||
"MsiBootstrapper"
|
||||
{
|
||||
"LangId" = "3:1033"
|
||||
"RequiresElevation" = "11:FALSE"
|
||||
}
|
||||
"Product"
|
||||
{
|
||||
"Name" = "8:Microsoft Visual Studio"
|
||||
"ProductName" = "8:UniGetUI"
|
||||
"ProductCode" = "8:{4896DCDD-E250-45EE-814B-0A6591D1AC00}"
|
||||
"PackageCode" = "8:{03CA204E-D817-4486-B0C3-C2A31BD2EBA1}"
|
||||
"UpgradeCode" = "8:{AA73EEF1-B0C1-4CDD-AA20-97C49E1085DA}"
|
||||
"AspNetVersion" = "8:2.0.50727.0"
|
||||
"RestartWWWService" = "11:FALSE"
|
||||
"RemovePreviousVersions" = "11:FALSE"
|
||||
"DetectNewerInstalledVersion" = "11:FALSE"
|
||||
"InstallAllUsers" = "11:TRUE"
|
||||
"ProductVersion" = "8:0.0.0"
|
||||
"Manufacturer" = "8:Martí Climent"
|
||||
"ARPHELPTELEPHONE" = "8:"
|
||||
"ARPHELPLINK" = "8:"
|
||||
"Title" = "8:UniGetUI"
|
||||
"Subject" = "8:"
|
||||
"ARPCONTACT" = "8:Martí Climent"
|
||||
"Keywords" = "8:"
|
||||
"ARPCOMMENTS" = "8:"
|
||||
"ARPURLINFOABOUT" = "8:"
|
||||
"ARPPRODUCTICON" = "8:"
|
||||
"ARPIconIndex" = "3:0"
|
||||
"SearchPath" = "8:"
|
||||
"UseSystemSearchPath" = "11:TRUE"
|
||||
"TargetPlatform" = "3:1"
|
||||
"PreBuildEvent" = "8:"
|
||||
"PostBuildEvent" = "8:"
|
||||
"RunPostBuildEvent" = "3:0"
|
||||
}
|
||||
"Registry"
|
||||
{
|
||||
"HKLM"
|
||||
{
|
||||
"Keys"
|
||||
{
|
||||
}
|
||||
}
|
||||
"HKCU"
|
||||
{
|
||||
"Keys"
|
||||
{
|
||||
}
|
||||
}
|
||||
"HKCR"
|
||||
{
|
||||
"Keys"
|
||||
{
|
||||
}
|
||||
}
|
||||
"HKU"
|
||||
{
|
||||
"Keys"
|
||||
{
|
||||
}
|
||||
}
|
||||
"HKPU"
|
||||
{
|
||||
"Keys"
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
"Sequences"
|
||||
{
|
||||
}
|
||||
"Shortcut"
|
||||
{
|
||||
}
|
||||
"UserInterface"
|
||||
{
|
||||
"{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_3A17EDF809CC4B78986B2FE6EA70BE3E"
|
||||
{
|
||||
"UseDynamicProperties" = "11:FALSE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdUserInterface.wim"
|
||||
}
|
||||
"{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_3D5999E6267F4C0298B4BCDE7D5F60A6"
|
||||
{
|
||||
"UseDynamicProperties" = "11:FALSE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdBasicDialogs.wim"
|
||||
}
|
||||
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_4DFD809C69BF4C59A6FD3BA01B0D95DA"
|
||||
{
|
||||
"Name" = "8:#1900"
|
||||
"Sequence" = "3:2"
|
||||
"Attributes" = "3:1"
|
||||
"Dialogs"
|
||||
{
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_1647D66DC7EA4BC4BE95CB839077FD2B"
|
||||
{
|
||||
"Sequence" = "3:200"
|
||||
"DisplayName" = "8:Installation Folder"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminFolderDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:0"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_4452C40CD596425EAB25E35D2B22C4CF"
|
||||
{
|
||||
"Sequence" = "3:100"
|
||||
"DisplayName" = "8:Welcome"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminWelcomeDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:0"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"CopyrightWarning"
|
||||
{
|
||||
"Name" = "8:CopyrightWarning"
|
||||
"DisplayName" = "8:#1002"
|
||||
"Description" = "8:#1102"
|
||||
"Type" = "3:3"
|
||||
"ContextData" = "8:"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:1"
|
||||
"Value" = "8:#1202"
|
||||
"DefaultValue" = "8:#1202"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"Welcome"
|
||||
{
|
||||
"Name" = "8:Welcome"
|
||||
"DisplayName" = "8:#1003"
|
||||
"Description" = "8:#1103"
|
||||
"Type" = "3:3"
|
||||
"ContextData" = "8:"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:1"
|
||||
"Value" = "8:#1203"
|
||||
"DefaultValue" = "8:#1203"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_56CAF5E0DFAE4764ACD49E3FD559D429"
|
||||
{
|
||||
"Sequence" = "3:300"
|
||||
"DisplayName" = "8:Confirm Installation"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminConfirmDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:0"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_5D2ED95DF08B4D95A7EB3239A89BF4C6"
|
||||
{
|
||||
"Name" = "8:#1902"
|
||||
"Sequence" = "3:2"
|
||||
"Attributes" = "3:3"
|
||||
"Dialogs"
|
||||
{
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_D56A5E75F9834D60948B9D0DD86D2299"
|
||||
{
|
||||
"Sequence" = "3:100"
|
||||
"DisplayName" = "8:Finished"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminFinishedDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:0"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_6EF9716EF936459ABC8F22F995A5CF35"
|
||||
{
|
||||
"Name" = "8:#1901"
|
||||
"Sequence" = "3:1"
|
||||
"Attributes" = "3:2"
|
||||
"Dialogs"
|
||||
{
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_2C4A093A2457487DBD623967D4706C28"
|
||||
{
|
||||
"Sequence" = "3:100"
|
||||
"DisplayName" = "8:Progress"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdProgressDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:0"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"ShowProgress"
|
||||
{
|
||||
"Name" = "8:ShowProgress"
|
||||
"DisplayName" = "8:#1009"
|
||||
"Description" = "8:#1109"
|
||||
"Type" = "3:5"
|
||||
"ContextData" = "8:1;True=1;False=0"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:0"
|
||||
"Value" = "3:1"
|
||||
"DefaultValue" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_DAA1A949544C4A96986254FF87C0D0D7"
|
||||
{
|
||||
"Name" = "8:#1901"
|
||||
"Sequence" = "3:2"
|
||||
"Attributes" = "3:2"
|
||||
"Dialogs"
|
||||
{
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_597DC589900D4E32B4D7BCB6B9B64387"
|
||||
{
|
||||
"Sequence" = "3:100"
|
||||
"DisplayName" = "8:Progress"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminProgressDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:0"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"ShowProgress"
|
||||
{
|
||||
"Name" = "8:ShowProgress"
|
||||
"DisplayName" = "8:#1009"
|
||||
"Description" = "8:#1109"
|
||||
"Type" = "3:5"
|
||||
"ContextData" = "8:1;True=1;False=0"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:0"
|
||||
"Value" = "3:1"
|
||||
"DefaultValue" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_F8B322222EFF4C19B40AB77367AC4879"
|
||||
{
|
||||
"Name" = "8:#1902"
|
||||
"Sequence" = "3:1"
|
||||
"Attributes" = "3:3"
|
||||
"Dialogs"
|
||||
{
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_6BB46602C9BA410C932C78D7334D7518"
|
||||
{
|
||||
"Sequence" = "3:100"
|
||||
"DisplayName" = "8:Finished"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdFinishedDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:0"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"UpdateText"
|
||||
{
|
||||
"Name" = "8:UpdateText"
|
||||
"DisplayName" = "8:#1058"
|
||||
"Description" = "8:#1158"
|
||||
"Type" = "3:15"
|
||||
"ContextData" = "8:"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:1"
|
||||
"Value" = "8:#1258"
|
||||
"DefaultValue" = "8:#1258"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_F90FA3407D9843D38E8B11EAEC8F1DF9"
|
||||
{
|
||||
"Name" = "8:#1900"
|
||||
"Sequence" = "3:1"
|
||||
"Attributes" = "3:1"
|
||||
"Dialogs"
|
||||
{
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_23F910A18BC2402DAFED0320959F69C5"
|
||||
{
|
||||
"Sequence" = "3:100"
|
||||
"DisplayName" = "8:Welcome"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdWelcomeDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:0"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"CopyrightWarning"
|
||||
{
|
||||
"Name" = "8:CopyrightWarning"
|
||||
"DisplayName" = "8:#1002"
|
||||
"Description" = "8:#1102"
|
||||
"Type" = "3:3"
|
||||
"ContextData" = "8:"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:1"
|
||||
"Value" = "8:#1202"
|
||||
"DefaultValue" = "8:#1202"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"Welcome"
|
||||
{
|
||||
"Name" = "8:Welcome"
|
||||
"DisplayName" = "8:#1003"
|
||||
"Description" = "8:#1103"
|
||||
"Type" = "3:3"
|
||||
"ContextData" = "8:"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:1"
|
||||
"Value" = "8:#1203"
|
||||
"DefaultValue" = "8:#1203"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_285DD2DB675346BEA1981B59FCF526E7"
|
||||
{
|
||||
"Sequence" = "3:300"
|
||||
"DisplayName" = "8:Confirm Installation"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdConfirmDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:0"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_9E00460782B64434AB9AE5C2FDF5F039"
|
||||
{
|
||||
"Sequence" = "3:200"
|
||||
"DisplayName" = "8:Installation Folder"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdFolderDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:0"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"InstallAllUsersVisible"
|
||||
{
|
||||
"Name" = "8:InstallAllUsersVisible"
|
||||
"DisplayName" = "8:#1059"
|
||||
"Description" = "8:#1159"
|
||||
"Type" = "3:5"
|
||||
"ContextData" = "8:1;True=1;False=0"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:0"
|
||||
"Value" = "3:1"
|
||||
"DefaultValue" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"MergeModule"
|
||||
{
|
||||
}
|
||||
"ProjectOutput"
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# How to create a MSI Installer for UniGetUI
|
||||
Sometimes, when deploying software through GPO, msi installers are required. However, UniGetUI does not offer such installers by default.
|
||||
In order to obtain a .msi installer, the following guide must be followed.
|
||||
|
||||
|
||||
> [!Warning]
|
||||
> When using MSI Installers, required dependencies will not be installed automatically. The deployer will need to ensure that the following requirements are met on target machines:
|
||||
> - Microsoft Visual C++ Redistriutable 2015-2022 (x64)
|
||||
> - Microsoft Edge WebView Runtime (x64)
|
||||
|
||||
## Creating a MSI wrapper for the installer
|
||||
1. Install [Visual Studio 2022](https://visualstudio.microsoft.com/es/downloads/) and the [Microsoft Visual Studio Installer Projects 2022 extension](https://marketplace.visualstudio.com/items?itemName=VisualStudioClient.MicrosoftVisualStudio2022InstallerProjects).
|
||||
2. Clone this repository:
|
||||
```
|
||||
git clone https://github.com/Devolutions/UniGetUI
|
||||
```
|
||||
3. Download from [GitHub](https://github.com/Devolutions/UniGetUI/releases/) the installer version you wish to package as MSI.
|
||||
4. Move the downloaded exe installer into `InstallerExtras/MsiCreator`. Ensure that the downloaded installer name is **exactly** `UniGetUI Installer.exe`
|
||||
5. Open the Solution (`MsiInstallerWrapper.sln`) with Visual Studio and build the solution. The files `UniGetUISetup.msi` and `setup.exe` will be created. You may want to delete `setup.exe`, since it will not be used.
|
||||
6. (Optional) Test that the file `UniGetUISetup.msi` installs UniGetUI properly. You should be now ready to go
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Martí Climent
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,240 @@
|
||||
# WARNING: **wingetui<sub>•</sub>com** and **unigetui<sub>•</sub>com** are fake websites hosted by a third-party. please do NOT trust them
|
||||
<br>
|
||||
|
||||
## <img src="media/icon.png" height="40">Devolutions UniGetUI
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Major announcement:** UniGetUI has entered its next chapter with Devolutions.
|
||||
> Read the [blog post](https://devolutions.net/blog/2026/03/unigetui-enters-its-next-chapter-with-devolutions/) and the [official press release](https://www.globenewswire.com/news-release/2026/03/10/3253012/0/en/Devolutions-Acquires-UniGetUI-Strengthening-Security-and-Enterprise-Readiness.html).
|
||||
|
||||
[](https://github.com/Devolutions/UniGetUI/releases/latest)
|
||||
[](https://github.com/Devolutions/UniGetUI/releases)
|
||||
[](https://github.com/Devolutions/UniGetUI/issues)
|
||||
[](https://github.com/Devolutions/UniGetUI/issues?q=is%3Aissue+is%3Aclosed)<br>
|
||||
UniGetUI is a Windows-first, intuitive GUI for the most common CLI package managers on Windows 10 and 11. Cross-platform builds are also available for macOS and Linux, with support for package managers including [WinGet](https://learn.microsoft.com/en-us/windows/package-manager/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/), [Homebrew](https://brew.sh/), [APT](https://wiki.debian.org/Apt), [DNF](https://dnf.readthedocs.io/), [Pacman](https://wiki.archlinux.org/title/Pacman), [Flatpak](https://flatpak.org/), [Snap](https://snapcraft.io/), [pip](https://pypi.org/), [npm](https://www.npmjs.com/), [Bun](https://bun.sh/), [.NET Tool](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-tool-install), [PowerShell Gallery](https://www.powershellgallery.com/), and more.
|
||||
With UniGetUI, you can discover, install, update, and uninstall software from multiple package managers through one interface.
|
||||
|
||||

|
||||
View more screenshots [here](#screenshots)
|
||||
|
||||
Check out the [Package Managers](#package-managers) section for more details!
|
||||
|
||||
**Disclaimer:** UniGetUI is not affiliated with the package managers it integrates with. Packages are provided by third parties, so review sources and publishers before installation.
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
> [!CAUTION]
|
||||
> **The official website for UniGetUI is [https://devolutions.net/unigetui/](https://devolutions.net/unigetui/).**<br>
|
||||
> **The official source repository is [https://github.com/Devolutions/UniGetUI](https://github.com/Devolutions/UniGetUI).**<br>
|
||||
> **Any other website should be considered unofficial, despite what they may say.**
|
||||
|
||||
🔒 Found a security issue? Please report it via the [Devolutions security page](https://devolutions.net/security/)
|
||||
|
||||
## Project stewardship
|
||||
|
||||
UniGetUI was created by Martí Climent and is now maintained by Devolutions. The project remains free, open source, and MIT-licensed. Devolutions' stewardship brings long-term investment, structured governance, stronger security processes, and a roadmap for broader enterprise readiness while keeping UniGetUI standalone and community-driven.
|
||||
|
||||
Read more in the [Devolutions announcement](https://devolutions.net/blog/2026/03/unigetui-enters-its-next-chapter-with-devolutions/) and the [official press release](https://www.globenewswire.com/news-release/2026/03/10/3253012/0/en/Devolutions-Acquires-UniGetUI-Strengthening-Security-and-Enterprise-Readiness.html).
|
||||
|
||||
## Table of contents
|
||||
- **[UniGetUI Homepage](https://devolutions.net/unigetui/)**
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [Installation](#installation)
|
||||
- [Update UniGetUI](#update-unigetui)
|
||||
- [Project stewardship](#project-stewardship)
|
||||
- [Features](#features)
|
||||
- [Package Managers](#package-managers)
|
||||
- [Translations](TRANSLATION.md)
|
||||
- [Contributions](#contributions)
|
||||
- [Screenshots](#screenshots)
|
||||
- [Frequently Asked Questions](#frequently-asked-questions)
|
||||
- [CLI reference](docs/CLI.md)
|
||||
- [IPC reference](docs/IPC.md)
|
||||
|
||||
## Installation
|
||||
<p>There are multiple ways to install UniGetUI — choose whichever one you prefer!</p>
|
||||
|
||||
### Windows
|
||||
|
||||
UniGetUI is primarily built for Windows. The Microsoft Store is the recommended installation method, but direct installer and package-manager options are also available.
|
||||
|
||||
#### Microsoft Store installation (recommended)
|
||||
<a href="https://apps.microsoft.com/detail/xpfftq032ptphf"><img alt="alt_text" width="240px" src="https://get.microsoft.com/images/en-us%20dark.svg" /></a>
|
||||
|
||||
#### Download the Windows installer:
|
||||

|
||||
Use the installer for the best Windows experience. `UniGetUI.Installer.exe` is the legacy/default x64 installer alias; use the explicit architecture downloads if needed.
|
||||
|
||||
| Architecture | Installer | Portable `.zip` |
|
||||
|---|---|---|
|
||||
| x64 | [UniGetUI.Installer.x64.exe](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.Installer.x64.exe) ([default x64 alias](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.Installer.exe)) | [UniGetUI.x64.zip](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.x64.zip) |
|
||||
| arm64 | [UniGetUI.Installer.arm64.exe](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.Installer.arm64.exe) | [UniGetUI.arm64.zip](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.arm64.zip) |
|
||||
|
||||
#### Install via WinGet:
|
||||

|
||||
```cmd
|
||||
winget install --exact --id Devolutions.UniGetUI --source winget
|
||||
```
|
||||
|
||||
|
||||
#### Install via Scoop:
|
||||

|
||||
```cmd
|
||||
scoop bucket add extras
|
||||
scoop install extras/unigetui
|
||||
```
|
||||
|
||||
#### Install via Chocolatey:
|
||||

|
||||
```cmd
|
||||
choco install unigetui
|
||||
```
|
||||
|
||||
### NativeAOT builds
|
||||
|
||||
Release packages are compiled with NativeAOT on all supported platforms (Windows, macOS, and Linux) for improved startup performance and a reduced attack surface. If you want to build the same configuration locally, the repository includes helper publish profiles and a script:
|
||||
|
||||
```powershell
|
||||
pwsh ./scripts/publish-nativeaot.ps1 -Platform x64 # or arm64
|
||||
```
|
||||
|
||||
This publishes `src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj` as a self-contained NativeAOT build into `artifacts/nativeaot/win-<platform>/`.
|
||||
|
||||
### macOS
|
||||
|
||||
macOS builds are available from GitHub Releases. Use the `.dmg` for the standard installer experience, or the `.tar.gz` archive for a portable app bundle.
|
||||
|
||||
| Architecture | `.dmg` | `.tar.gz` |
|
||||
|---|---|---|
|
||||
| Apple silicon (arm64) | [UniGetUI.macos-arm64.dmg](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.macos-arm64.dmg) | [UniGetUI.macos-arm64.tar.gz](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.macos-arm64.tar.gz) |
|
||||
| Intel (x64) | [UniGetUI.macos-x64.dmg](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.macos-x64.dmg) | [UniGetUI.macos-x64.tar.gz](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.macos-x64.tar.gz) |
|
||||
|
||||
### Linux
|
||||
|
||||
Linux builds are available from GitHub Releases. Use the `.deb` package for Debian/Ubuntu-based distributions, the `.rpm` package for Fedora/RHEL-based distributions, or the `.tar.gz` archive for a portable build.
|
||||
|
||||
| Architecture | `.deb` | `.rpm` | `.tar.gz` |
|
||||
|---|---|---|---|
|
||||
| x64 | [Download](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.linux-x64.deb) | [Download](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.linux-x64.rpm) | [Download](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.linux-x64.tar.gz) |
|
||||
| arm64 | [Download](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.linux-arm64.deb) | [Download](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.linux-arm64.rpm) | [Download](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.linux-arm64.tar.gz) |
|
||||
|
||||
Install the package that matches your distribution and architecture:
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu-based distributions
|
||||
sudo apt install ./UniGetUI.linux-x64.deb
|
||||
|
||||
# Fedora/RHEL-based distributions
|
||||
sudo dnf install ./UniGetUI.linux-x64.rpm
|
||||
|
||||
# Portable archive
|
||||
tar -xzf UniGetUI.linux-x64.tar.gz
|
||||
./UniGetUI
|
||||
```
|
||||
|
||||
Replace `x64` with `arm64` in the file name when using the arm64 build.
|
||||
|
||||
|
||||
## Update UniGetUI
|
||||
|
||||
UniGetUI has a built-in autoupdater. On Windows, it can also be updated like any other package within UniGetUI when installed through WinGet, Scoop, or Chocolatey.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- Install, update, and remove software from your system easily at one click: UniGetUI combines packages from the most used package managers for your platform, including WinGet, Chocolatey, Scoop, Homebrew, APT, DNF, Pacman, Flatpak, Snap, Pip, npm, Bun, and .NET Tool.
|
||||
- Discover new packages and filter them to easily find the package you want.
|
||||
- View detailed metadata about any package before installing it. Get the direct download URL or the name of the publisher, as well as the size of the download.
|
||||
- Easily bulk-install, update, or uninstall multiple packages at once selecting multiple packages before performing an operation
|
||||
- Automatically update packages, or be notified when updates become available. Skip versions or completely ignore updates on a per-package basis.
|
||||
- The system tray icon will also show the available updates and installed packages where supported, to efficiently update a program or remove a package from your system.
|
||||
- Easily customize how and where packages are installed. Select different installation options and switches for each package. Install an older version or force a specific architecture where supported. \[But don't worry, those options will be saved for future updates for this package*]
|
||||
- Share packages with your friends using generated package links.
|
||||
- Export custom lists of packages to then import them to another machine and install those packages with previously specified, custom installation parameters. Setting up machines or configuring a specific software setup has never been easier.
|
||||
- Backup your packages to a local file to easily recover your setup in a matter of seconds when migrating to a new machine*
|
||||
|
||||
## Package Managers
|
||||
|
||||
**NOTE:** All package managers do support basic install, update, and uninstall processes, as well as checking for updates, finding new packages, and retrieving details from a package.
|
||||
|
||||
UniGetUI loads package managers based on the current platform. Windows builds include WinGet, Scoop, Chocolatey, Windows PowerShell, PowerShell 7, npm, Bun, pip, Cargo, .NET Tool, and vcpkg. macOS and Linux builds include Homebrew, PowerShell 7, npm, Bun, pip, Cargo, .NET Tool, and vcpkg; Linux builds also include distro-aware support for APT, DNF, Pacman, Snap, and Flatpak where applicable.
|
||||
|
||||

|
||||
|
||||
✅: Supported on UniGetUI<br>
|
||||
☑️: Not directly supported but can be easily achieved<br>
|
||||
⚠️: May not work in some cases<br>
|
||||
❌: Not supported by the Package Manager<br>
|
||||
<br>
|
||||
|
||||
## Translations
|
||||
|
||||
UniGetUI translations are maintained directly in this repository. For the current language list, completion status, and per-language contributor attributions, see [TRANSLATION.md](TRANSLATION.md). If you spot a translation issue or want to improve a locale, please open an issue or submit a pull request.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
## Contributions
|
||||
UniGetUI continues to grow thanks to its community of contributors. Devolutions is grateful to everyone who contributes code, translations, documentation, testing, and feedback to the project.<br><br>
|
||||
|
||||
[](https://github.com/Devolutions/UniGetUI/graphs/contributors)<br><br>
|
||||
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
**Q: I am unable to install or upgrade a specific Winget package! What should I do?**<br>
|
||||
|
||||
A: This is likely an issue with Winget rather than UniGetUI.
|
||||
|
||||
Please check if it's possible to install/upgrade the package through PowerShell or the Command Prompt by using the commands `winget upgrade` or `winget install`, depending on the situation (for example: `winget upgrade --id Microsoft.PowerToys`).
|
||||
|
||||
If this doesn't work, consider asking for help at [Winget's project page](https://github.com/microsoft/winget-cli).<br>
|
||||
|
||||
#
|
||||
|
||||
**Q: The name of a package is trimmed with ellipsis — how do I see its full name/id?**<br>
|
||||
|
||||
A: This is a known limitation of Winget.
|
||||
|
||||
For more details, see this issue: https://github.com/microsoft/winget-cli/issues/2603.<br>
|
||||
|
||||
#
|
||||
|
||||
**Q: My antivirus is telling me that UniGetUI is a virus! / My browser is blocking the download of UniGetUI!**<br>
|
||||
|
||||
A: A common reason apps (i.e., executables) get blocked and/or detected as a virus — even when there's nothing malicious about them, like in the case of UniGetUI — is because a relatively large amount of people are not using them.
|
||||
|
||||
Combine that with the fact that you might be downloading something recently released, and blocking unknown apps is in many cases a good precaution to take to prevent actual malware.
|
||||
|
||||
Since UniGetUI is open source and safe to use, whitelist the app in the settings of your antivirus/browser.<br>
|
||||
|
||||
#
|
||||
|
||||
**Q: Are packages from third-party package managers safe?**<br>
|
||||
|
||||
A: UniGetUI and the package-manager maintainers aren't responsible for every package available for download. Packages are provided by third parties and can theoretically be compromised, regardless of whether they come from WinGet, Scoop, Chocolatey, Homebrew, APT, DNF, Pacman, Flatpak, Snap, or another source.
|
||||
|
||||
Some package managers and repositories implement checks to mitigate the risks of downloading malware. Even so, it's recommended that you only download software from trusted publishers.
|
||||
|
||||
## Command-line interface:
|
||||
|
||||
Check out the CLI reference [here](docs/CLI.md) and the IPC reference [here](docs/IPC.md).
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`Devolutions/UniGetUI`
|
||||
- 原始仓库:https://github.com/Devolutions/UniGetUI
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Found a security issue? Please report it via the [Devolutions security page](https://devolutions.net/security/).
|
||||
@@ -0,0 +1,156 @@
|
||||
# Translations
|
||||
|
||||
UniGetUI includes translations for the languages listed below.
|
||||
|
||||
This page lists the supported languages, each locale file, current completion status, and the credited contributors for each translation.
|
||||
|
||||
If you would like to help improve a translation or report an issue, please open an issue or submit a pull request.
|
||||
|
||||
Translation discussion and coordination also happens in [GitHub discussion #4510](https://github.com/Devolutions/UniGetUI/discussions/4510).
|
||||
|
||||
## Language Coverage
|
||||
|
||||
| Language | Code | Translated | File |
|
||||
| :-- | :-- | :-- | :-- |
|
||||
| <img src='https://flagcdn.com/za.svg' width=20> Afrikaans - Afrikaans | `af` | 100% | [lang_af.json](src/Languages/lang_af.json) |
|
||||
| <img src='https://flagcdn.com/sa.svg' width=20> Arabic - عربي | `ar` | 100% | [lang_ar.json](src/Languages/lang_ar.json) |
|
||||
| <img src='https://flagcdn.com/by.svg' width=20> Belarusian - беларуская | `be` | 100% | [lang_be.json](src/Languages/lang_be.json) |
|
||||
| <img src='https://flagcdn.com/bg.svg' width=20> Bulgarian - български | `bg` | 100% | [lang_bg.json](src/Languages/lang_bg.json) |
|
||||
| <img src='https://flagcdn.com/bd.svg' width=20> Bangla - বাংলা | `bn` | 100% | [lang_bn.json](src/Languages/lang_bn.json) |
|
||||
| <img src='https://flagcdn.com/ad.svg' width=20> Catalan - Català | `ca` | 100% | [lang_ca.json](src/Languages/lang_ca.json) |
|
||||
| <img src='https://flagcdn.com/cz.svg' width=20> Czech - Čeština | `cs` | 100% | [lang_cs.json](src/Languages/lang_cs.json) |
|
||||
| <img src='https://flagcdn.com/dk.svg' width=20> Danish - Dansk | `da` | 99% | [lang_da.json](src/Languages/lang_da.json) |
|
||||
| <img src='https://flagcdn.com/de.svg' width=20> German - Deutsch | `de` | 100% | [lang_de.json](src/Languages/lang_de.json) |
|
||||
| <img src='https://flagcdn.com/gr.svg' width=20> Greek - Ελληνικά | `el` | 100% | [lang_el.json](src/Languages/lang_el.json) |
|
||||
| <img src='https://flagcdn.com/ee.svg' width=20> Estonian - Eesti | `et` | 100% | [lang_et.json](src/Languages/lang_et.json) |
|
||||
| <img src='https://flagcdn.com/gb.svg' width=20> English - English | `en` | 100% | [lang_en.json](src/Languages/lang_en.json) |
|
||||
| <img src='https://upload.wikimedia.org/wikipedia/commons/f/f5/Flag_of_Esperanto.svg' width=20> Esperanto - Esperanto | `eo` | 99% | [lang_eo.json](src/Languages/lang_eo.json) |
|
||||
| <img src='https://flagcdn.com/es.svg' width=20> Spanish - Castellano | `es` | 100% | [lang_es.json](src/Languages/lang_es.json) |
|
||||
| <img src='https://flagcdn.com/mx.svg' width=20> Spanish (Mexico) | `es-MX` | 100% | [lang_es-MX.json](src/Languages/lang_es-MX.json) |
|
||||
| <img src='https://flagcdn.com/ir.svg' width=20> Persian - فارسی | `fa` | 100% | [lang_fa.json](src/Languages/lang_fa.json) |
|
||||
| <img src='https://flagcdn.com/fi.svg' width=20> Finnish - Suomi | `fi` | 100% | [lang_fi.json](src/Languages/lang_fi.json) |
|
||||
| <img src='https://flagcdn.com/ph.svg' width=20> Filipino - Filipino | `fil` | 99% | [lang_fil.json](src/Languages/lang_fil.json) |
|
||||
| <img src='https://flagcdn.com/fr.svg' width=20> French - Français | `fr` | 100% | [lang_fr.json](src/Languages/lang_fr.json) |
|
||||
| <img src='https://flagcdn.com/es.svg' width=20> Galician - Galego | `gl` | 99% | [lang_gl.json](src/Languages/lang_gl.json) |
|
||||
| <img src='https://flagcdn.com/in.svg' width=20> Gujarati - ગુજરાતી | `gu` | 100% | [lang_gu.json](src/Languages/lang_gu.json) |
|
||||
| <img src='https://flagcdn.com/in.svg' width=20> Hindi - हिंदी | `hi` | 100% | [lang_hi.json](src/Languages/lang_hi.json) |
|
||||
| <img src='https://flagcdn.com/hr.svg' width=20> Croatian - Hrvatski | `hr` | 100% | [lang_hr.json](src/Languages/lang_hr.json) |
|
||||
| <img src='https://flagcdn.com/il.svg' width=20> Hebrew - עִבְרִית | `he` | 100% | [lang_he.json](src/Languages/lang_he.json) |
|
||||
| <img src='https://flagcdn.com/hu.svg' width=20> Hungarian - Magyar | `hu` | 100% | [lang_hu.json](src/Languages/lang_hu.json) |
|
||||
| <img src='https://flagcdn.com/it.svg' width=20> Italian - Italiano | `it` | 100% | [lang_it.json](src/Languages/lang_it.json) |
|
||||
| <img src='https://flagcdn.com/id.svg' width=20> Indonesian - Bahasa Indonesia | `id` | 100% | [lang_id.json](src/Languages/lang_id.json) |
|
||||
| <img src='https://flagcdn.com/jp.svg' width=20> Japanese - 日本語 | `ja` | 100% | [lang_ja.json](src/Languages/lang_ja.json) |
|
||||
| <img src='https://flagcdn.com/ge.svg' width=20> Georgian - ქართული | `ka` | 100% | [lang_ka.json](src/Languages/lang_ka.json) |
|
||||
| <img src='https://flagcdn.com/in.svg' width=20> Kannada - ಕನ್ನಡ | `kn` | 100% | [lang_kn.json](src/Languages/lang_kn.json) |
|
||||
| <img src='https://flagcdn.com/kr.svg' width=20> Korean - 한국어 | `ko` | 100% | [lang_ko.json](src/Languages/lang_ko.json) |
|
||||
| <img src='https://flagcdn.com/iq.svg' width=20> Kurdish - کوردی | `ku` | 100% | [lang_ku.json](src/Languages/lang_ku.json) |
|
||||
| <img src='https://flagcdn.com/lt.svg' width=20> Lithuanian - Lietuvių | `lt` | 100% | [lang_lt.json](src/Languages/lang_lt.json) |
|
||||
| <img src='https://flagcdn.com/mk.svg' width=20> Macedonian - Македонски | `mk` | 100% | [lang_mk.json](src/Languages/lang_mk.json) |
|
||||
| <img src='https://flagcdn.com/in.svg' width=20> Marathi - मराठी | `mr` | 100% | [lang_mr.json](src/Languages/lang_mr.json) |
|
||||
| <img src='https://flagcdn.com/no.svg' width=20> Norwegian (bokmål) | `nb` | 100% | [lang_nb.json](src/Languages/lang_nb.json) |
|
||||
| <img src='https://flagcdn.com/no.svg' width=20> Norwegian (nynorsk) | `nn` | 100% | [lang_nn.json](src/Languages/lang_nn.json) |
|
||||
| <img src='https://flagcdn.com/nl.svg' width=20> Dutch - Nederlands | `nl` | 100% | [lang_nl.json](src/Languages/lang_nl.json) |
|
||||
| <img src='https://flagcdn.com/pl.svg' width=20> Polish - Polski | `pl` | 100% | [lang_pl.json](src/Languages/lang_pl.json) |
|
||||
| <img src='https://flagcdn.com/br.svg' width=20> Portuguese (Brazil) | `pt_BR` | 100% | [lang_pt_BR.json](src/Languages/lang_pt_BR.json) |
|
||||
| <img src='https://flagcdn.com/pt.svg' width=20> Portuguese (Portugal) | `pt_PT` | 100% | [lang_pt_PT.json](src/Languages/lang_pt_PT.json) |
|
||||
| <img src='https://flagcdn.com/ro.svg' width=20> Romanian - Română | `ro` | 100% | [lang_ro.json](src/Languages/lang_ro.json) |
|
||||
| <img src='https://flagcdn.com/ru.svg' width=20> Russian - Русский | `ru` | 100% | [lang_ru.json](src/Languages/lang_ru.json) |
|
||||
| <img src='https://flagcdn.com/in.svg' width=20> Sanskrit - संस्कृत भाषा | `sa` | 100% | [lang_sa.json](src/Languages/lang_sa.json) |
|
||||
| <img src='https://flagcdn.com/sk.svg' width=20> Slovak - Slovenčina | `sk` | 100% | [lang_sk.json](src/Languages/lang_sk.json) |
|
||||
| <img src='https://flagcdn.com/rs.svg' width=20> Serbian - Srpski | `sr` | 100% | [lang_sr.json](src/Languages/lang_sr.json) |
|
||||
| <img src='https://flagcdn.com/al.svg' width=20> Albanian - Shqip | `sq` | 100% | [lang_sq.json](src/Languages/lang_sq.json) |
|
||||
| <img src='https://flagcdn.com/lk.svg' width=20> Sinhala - සිංහල | `si` | 100% | [lang_si.json](src/Languages/lang_si.json) |
|
||||
| <img src='https://flagcdn.com/si.svg' width=20> Slovene - Slovenščina | `sl` | 100% | [lang_sl.json](src/Languages/lang_sl.json) |
|
||||
| <img src='https://flagcdn.com/se.svg' width=20> Swedish - Svenska | `sv` | 100% | [lang_sv.json](src/Languages/lang_sv.json) |
|
||||
| <img src='https://flagcdn.com/in.svg' width=20> Tamil - தமிழ் | `ta` | 100% | [lang_ta.json](src/Languages/lang_ta.json) |
|
||||
| <img src='https://flagcdn.com/tl.svg' width=20> Tagalog - Tagalog | `tl` | 100% | [lang_tl.json](src/Languages/lang_tl.json) |
|
||||
| <img src='https://flagcdn.com/th.svg' width=20> Thai - ภาษาไทย | `th` | 100% | [lang_th.json](src/Languages/lang_th.json) |
|
||||
| <img src='https://flagcdn.com/tr.svg' width=20> Turkish - Türkçe | `tr` | 100% | [lang_tr.json](src/Languages/lang_tr.json) |
|
||||
| <img src='https://flagcdn.com/uk.svg' width=20> Ukrainian - Українська | `uk` | 100% | [lang_uk.json](src/Languages/lang_uk.json) |
|
||||
| <img src='https://flagcdn.com/pk.svg' width=20> Urdu - اردو | `ur` | 100% | [lang_ur.json](src/Languages/lang_ur.json) |
|
||||
| <img src='https://flagcdn.com/vn.svg' width=20> Vietnamese - Tiếng Việt | `vi` | 100% | [lang_vi.json](src/Languages/lang_vi.json) |
|
||||
| <img src='https://flagcdn.com/cn.svg' width=20> Simplified Chinese (China) | `zh_CN` | 100% | [lang_zh_CN.json](src/Languages/lang_zh_CN.json) |
|
||||
| <img src='https://flagcdn.com/tw.svg' width=20> Traditional Chinese (Taiwan) | `zh_TW` | 100% | [lang_zh_TW.json](src/Languages/lang_zh_TW.json) |
|
||||
|
||||
## Contributors
|
||||
|
||||
We are grateful to everyone who contributes translations to UniGetUI. Contributor credits are sourced from [Translators.json](src/Languages/Data/Translators.json). If you would like to be added to or removed from the list for a particular language, please open a pull request.
|
||||
|
||||
| Language | Code | Contributor(s) |
|
||||
| :-- | :-- | --- |
|
||||
| <img src='https://flagcdn.com/za.svg' width=20> Afrikaans - Afrikaans | `af` | Hendrik Bezuidenhout |
|
||||
| <img src='https://flagcdn.com/sa.svg' width=20> Arabic - عربي | `ar` | [Abdu11ahAS](https://github.com/Abdu11ahAS), [Abdullah-Dev115](https://github.com/Abdullah-Dev115), [AbdullahAlousi](https://github.com/AbdullahAlousi), [bassuny3003](https://github.com/bassuny3003), [DaRandomCube](https://github.com/DaRandomCube), [FancyCookin](https://github.com/FancyCookin), [IFrxo](https://github.com/IFrxo), [mo9a7i](https://github.com/mo9a7i) |
|
||||
| <img src='https://flagcdn.com/by.svg' width=20> Belarusian - беларуская | `be` | [bthos](https://github.com/bthos) |
|
||||
| <img src='https://flagcdn.com/bg.svg' width=20> Bulgarian - български | `bg` | Nikolay Naydenov, Vasil Kolev |
|
||||
| <img src='https://flagcdn.com/bd.svg' width=20> Bangla - বাংলা | `bn` | [fluentmoheshwar](https://github.com/fluentmoheshwar), [itz-rj-here](https://github.com/itz-rj-here), Mushfiq Iqbal Rayon, Nilavra Bhattacharya, [samiulislamsharan](https://github.com/samiulislamsharan) |
|
||||
| <img src='https://flagcdn.com/ad.svg' width=20> Catalan - Català | `ca` | [marticliment](https://github.com/marticliment) |
|
||||
| <img src='https://flagcdn.com/cz.svg' width=20> Czech - Čeština | `cs` | [mlisko](https://github.com/mlisko), [panther7](https://github.com/panther7), [xtorlukas](https://github.com/xtorlukas) |
|
||||
| <img src='https://flagcdn.com/dk.svg' width=20> Danish - Dansk | `da` | [AAUCrisp](https://github.com/AAUCrisp), [bstordrup](https://github.com/bstordrup), [mikkolukas](https://github.com/mikkolukas), [siewers](https://github.com/siewers), [yrjarv](https://github.com/yrjarv) |
|
||||
| <img src='https://flagcdn.com/de.svg' width=20> German - Deutsch | `de` | [1270o1](https://github.com/1270o1), [AbsolutLeon](https://github.com/AbsolutLeon), [alxhu-dev](https://github.com/alxhu-dev), [Araxxas](https://github.com/Araxxas), [arnowelzel](https://github.com/arnowelzel), [CanePlayz](https://github.com/CanePlayz), [Datacra5H](https://github.com/Datacra5H), [ebnater](https://github.com/ebnater), [lucadsign](https://github.com/lucadsign), [martinwilco](https://github.com/martinwilco), [michaelmairegger](https://github.com/michaelmairegger), [Seeloewen](https://github.com/Seeloewen), [TheScarfix](https://github.com/TheScarfix), [tkohlmeier](https://github.com/tkohlmeier), [VfBFan](https://github.com/VfBFan), [Xeraox3335](https://github.com/Xeraox3335), [yrjarv](https://github.com/yrjarv) |
|
||||
| <img src='https://flagcdn.com/gr.svg' width=20> Greek - Ελληνικά | `el` | [antwnhsx](https://github.com/antwnhsx), [panos78](https://github.com/panos78), [seijind](https://github.com/seijind), [thunderstrike116](https://github.com/thunderstrike116), [wobblerrrgg](https://github.com/wobblerrrgg) |
|
||||
| <img src='https://flagcdn.com/ee.svg' width=20> Estonian - Eesti | `et` | [artjom3729](https://github.com/artjom3729) |
|
||||
| <img src='https://flagcdn.com/gb.svg' width=20> English - English | `en` | [lucadsign](https://github.com/lucadsign), [marticliment](https://github.com/marticliment), [ppvnf](https://github.com/ppvnf) |
|
||||
| <img src='https://upload.wikimedia.org/wikipedia/commons/f/f5/Flag_of_Esperanto.svg' width=20> Esperanto - Esperanto | `eo` | |
|
||||
| <img src='https://flagcdn.com/es.svg' width=20> Spanish - Castellano | `es` | [apazga](https://github.com/apazga), [dalbitresb12](https://github.com/dalbitresb12), [evaneliasyoung](https://github.com/evaneliasyoung), [guplem](https://github.com/guplem), [JMoreno97](https://github.com/JMoreno97), [marticliment](https://github.com/marticliment), [P10Designs](https://github.com/P10Designs), [rubnium](https://github.com/rubnium), [uKER](https://github.com/uKER) |
|
||||
| <img src='https://flagcdn.com/mx.svg' width=20> Spanish (Mexico) | `es-MX` | [apazga](https://github.com/apazga), [dalbitresb12](https://github.com/dalbitresb12), [evaneliasyoung](https://github.com/evaneliasyoung), [guplem](https://github.com/guplem), [JMoreno97](https://github.com/JMoreno97), [marticliment](https://github.com/marticliment), [P10Designs](https://github.com/P10Designs), [rubnium](https://github.com/rubnium), [uKER](https://github.com/uKER) |
|
||||
| <img src='https://flagcdn.com/ir.svg' width=20> Persian - فارسی | `fa` | [ehinium](https://github.com/ehinium), [MobinMardi](https://github.com/MobinMardi) |
|
||||
| <img src='https://flagcdn.com/fi.svg' width=20> Finnish - Suomi | `fi` | [simakuutio](https://github.com/simakuutio) |
|
||||
| <img src='https://flagcdn.com/ph.svg' width=20> Filipino - Filipino | `fil` | [infyProductions](https://github.com/infyProductions) |
|
||||
| <img src='https://flagcdn.com/fr.svg' width=20> French - Français | `fr` | BreatFR, [Entropiness](https://github.com/Entropiness), Evans Costa, [PikPakPik](https://github.com/PikPakPik), Rémi Guerrero, [W1L7dev](https://github.com/W1L7dev) |
|
||||
| <img src='https://flagcdn.com/es.svg' width=20> Galician - Galego | `gl` | |
|
||||
| <img src='https://flagcdn.com/in.svg' width=20> Gujarati - ગુજરાતી | `gu` | |
|
||||
| <img src='https://flagcdn.com/in.svg' width=20> Hindi - हिंदी | `hi` | [Ashu-r](https://github.com/Ashu-r), [atharva_xoxo](https://github.com/atharva_xoxo), [satanarious](https://github.com/satanarious) |
|
||||
| <img src='https://flagcdn.com/hr.svg' width=20> Croatian - Hrvatski | `hr` | [AndrejFeher](https://github.com/AndrejFeher), Ivan Nuić, Stjepan Treger |
|
||||
| <img src='https://flagcdn.com/il.svg' width=20> Hebrew - עִבְרִית | `he` | [maximunited](https://github.com/maximunited), Oryan Hassidim |
|
||||
| <img src='https://flagcdn.com/hu.svg' width=20> Hungarian - Magyar | `hu` | [gidano](https://github.com/gidano) |
|
||||
| <img src='https://flagcdn.com/it.svg' width=20> Italian - Italiano | `it` | David Senoner, [giacobot](https://github.com/giacobot), [maicol07](https://github.com/maicol07), [mapi68](https://github.com/mapi68), [mrfranza](https://github.com/mrfranza), Rosario Di Mauro |
|
||||
| <img src='https://flagcdn.com/id.svg' width=20> Indonesian - Bahasa Indonesia | `id` | [agrinfauzi](https://github.com/agrinfauzi), [arthackrc](https://github.com/arthackrc), [joenior](https://github.com/joenior), [nrarfn](https://github.com/nrarfn) |
|
||||
| <img src='https://flagcdn.com/jp.svg' width=20> Japanese - 日本語 | `ja` | [anmoti](https://github.com/anmoti), [BHCrusher1](https://github.com/BHCrusher1), [nob-swik](https://github.com/nob-swik), Nobuhiro Shintaku, sho9029, [tacostea](https://github.com/tacostea), Yuki Takase |
|
||||
| <img src='https://flagcdn.com/ge.svg' width=20> Georgian - ქართული | `ka` | [marticliment](https://github.com/marticliment), [ppvnf](https://github.com/ppvnf) |
|
||||
| <img src='https://flagcdn.com/in.svg' width=20> Kannada - ಕನ್ನಡ | `kn` | [skanda890](https://github.com/skanda890) |
|
||||
| <img src='https://flagcdn.com/kr.svg' width=20> Korean - 한국어 | `ko` | [VenusGirl](https://github.com/VenusGirl) |
|
||||
| <img src='https://flagcdn.com/iq.svg' width=20> Kurdish - کوردی | `ku` | |
|
||||
| <img src='https://flagcdn.com/lt.svg' width=20> Lithuanian - Lietuvių | `lt` | Džiugas Januševičius, [dziugas1959](https://github.com/dziugas1959), [martyn3z](https://github.com/martyn3z) |
|
||||
| <img src='https://flagcdn.com/mk.svg' width=20> Macedonian - Македонски | `mk` | LordDeatHunter |
|
||||
| <img src='https://flagcdn.com/in.svg' width=20> Marathi - मराठी | `mr` | |
|
||||
| <img src='https://flagcdn.com/no.svg' width=20> Norwegian (bokmål) | `nb` | [DandelionSprout](https://github.com/DandelionSprout), [mikaelkw](https://github.com/mikaelkw), [yrjarv](https://github.com/yrjarv) |
|
||||
| <img src='https://flagcdn.com/no.svg' width=20> Norwegian (nynorsk) | `nn` | [yrjarv](https://github.com/yrjarv) |
|
||||
| <img src='https://flagcdn.com/nl.svg' width=20> Dutch - Nederlands | `nl` | [abbydiode](https://github.com/abbydiode), [CateyeNL](https://github.com/CateyeNL), [mia-riezebos](https://github.com/mia-riezebos), [Stephan-P](https://github.com/Stephan-P) |
|
||||
| <img src='https://flagcdn.com/pl.svg' width=20> Polish - Polski | `pl` | [AdiMajsterek](https://github.com/AdiMajsterek), [GrzegorzKi](https://github.com/GrzegorzKi), [H4qu3r](https://github.com/H4qu3r), [ikarmus2001](https://github.com/ikarmus2001), [juliazero](https://github.com/juliazero), [KamilZielinski](https://github.com/KamilZielinski), [kwiateusz](https://github.com/kwiateusz), [RegularGvy13](https://github.com/RegularGvy13), [szumsky](https://github.com/szumsky), [ThePhaseless](https://github.com/ThePhaseless) |
|
||||
| <img src='https://flagcdn.com/br.svg' width=20> Portuguese (Brazil) | `pt_BR` | [maisondasilva](https://github.com/maisondasilva), [ppvnf](https://github.com/ppvnf), [renanalencar](https://github.com/renanalencar), [Rodrigo-Matsuura](https://github.com/Rodrigo-Matsuura), [thiagojramos](https://github.com/thiagojramos), [wanderleihuttel](https://github.com/wanderleihuttel) |
|
||||
| <img src='https://flagcdn.com/pt.svg' width=20> Portuguese (Portugal) | `pt_PT` | [100Nome](https://github.com/100Nome), [NimiGames68](https://github.com/NimiGames68), [PoetaGA](https://github.com/PoetaGA), [Putocoroa](https://github.com/Putocoroa), [Tiago_Ferreira](https://github.com/Tiago_Ferreira) |
|
||||
| <img src='https://flagcdn.com/ro.svg' width=20> Romanian - Română | `ro` | [David735453](https://github.com/David735453), [lucadsign](https://github.com/lucadsign), [SilverGreen93](https://github.com/SilverGreen93), TZACANEL |
|
||||
| <img src='https://flagcdn.com/ru.svg' width=20> Russian - Русский | `ru` | Alexander, [bropines](https://github.com/bropines), [Denisskas](https://github.com/Denisskas), [DvladikD](https://github.com/DvladikD), [flatron4eg](https://github.com/flatron4eg), Gleb Saygin, [katrovsky](https://github.com/katrovsky), Sergey, [sklart](https://github.com/sklart), [solarscream](https://github.com/solarscream), [tapnisu](https://github.com/tapnisu), [Vertuhai](https://github.com/Vertuhai) |
|
||||
| <img src='https://flagcdn.com/in.svg' width=20> Sanskrit - संस्कृत भाषा | `sa` | [skanda890](https://github.com/skanda890) |
|
||||
| <img src='https://flagcdn.com/sk.svg' width=20> Slovak - Slovenčina | `sk` | [david-kucera](https://github.com/david-kucera), [Luk164](https://github.com/Luk164) |
|
||||
| <img src='https://flagcdn.com/rs.svg' width=20> Serbian - Srpski | `sr` | [daVinci13](https://github.com/daVinci13), [igorskyflyer](https://github.com/igorskyflyer), [momcilovicluka](https://github.com/momcilovicluka) |
|
||||
| <img src='https://flagcdn.com/al.svg' width=20> Albanian - Shqip | `sq` | [RDN000](https://github.com/RDN000) |
|
||||
| <img src='https://flagcdn.com/lk.svg' width=20> Sinhala - සිංහල | `si` | [SashikaSandeepa](https://github.com/SashikaSandeepa), [Savithu-s3](https://github.com/Savithu-s3), [ttheek](https://github.com/ttheek) |
|
||||
| <img src='https://flagcdn.com/si.svg' width=20> Slovene - Slovenščina | `sl` | [rumplin](https://github.com/rumplin) |
|
||||
| <img src='https://flagcdn.com/se.svg' width=20> Swedish - Svenska | `sv` | [curudel](https://github.com/curudel), [Hi-there-how-are-u](https://github.com/Hi-there-how-are-u), [kakmonster](https://github.com/kakmonster), [umeaboy](https://github.com/umeaboy) |
|
||||
| <img src='https://flagcdn.com/in.svg' width=20> Tamil - தமிழ் | `ta` | [nochilli](https://github.com/nochilli) |
|
||||
| <img src='https://flagcdn.com/tl.svg' width=20> Tagalog - Tagalog | `tl` | lasersPew, [znarfm](https://github.com/znarfm) |
|
||||
| <img src='https://flagcdn.com/th.svg' width=20> Thai - ภาษาไทย | `th` | [apaeisara](https://github.com/apaeisara), [dulapahv](https://github.com/dulapahv), [hanchain](https://github.com/hanchain), [rikoprushka](https://github.com/rikoprushka), [vestearth](https://github.com/vestearth) |
|
||||
| <img src='https://flagcdn.com/tr.svg' width=20> Turkish - Türkçe | `tr` | [ahmetozmtn](https://github.com/ahmetozmtn), [anzeralp](https://github.com/anzeralp), [BerkeA111](https://github.com/BerkeA111), [dogancanyr](https://github.com/dogancanyr), [gokberkgs](https://github.com/gokberkgs) |
|
||||
| <img src='https://flagcdn.com/uk.svg' width=20> Ukrainian - Українська | `uk` | Alex Logvin, Artem Moldovanenko, Operator404, [Taron-art](https://github.com/Taron-art), [Vertuhai](https://github.com/Vertuhai) |
|
||||
| <img src='https://flagcdn.com/pk.svg' width=20> Urdu - اردو | `ur` | [digitio](https://github.com/digitio), [digitpk](https://github.com/digitpk), [hamzaharoon1314](https://github.com/hamzaharoon1314) |
|
||||
| <img src='https://flagcdn.com/vn.svg' width=20> Vietnamese - Tiếng Việt | `vi` | [aethervn2309](https://github.com/aethervn2309), [legendsjoon](https://github.com/legendsjoon), [txavlog](https://github.com/txavlog), [vanlongluuly](https://github.com/vanlongluuly) |
|
||||
| <img src='https://flagcdn.com/cn.svg' width=20> Simplified Chinese (China) | `zh_CN` | Aaron Liu, [adfnekc](https://github.com/adfnekc), [Ardenet](https://github.com/Ardenet), [arthurfsy2](https://github.com/arthurfsy2), [bai0012](https://github.com/bai0012), BUGP Association, ciaran, CnYeSheng, Cololi, [dongfengweixiao](https://github.com/dongfengweixiao), [enKl03B](https://github.com/enKl03B), [seanyu0](https://github.com/seanyu0), [Sigechaishijie](https://github.com/Sigechaishijie), [SpaceTimee](https://github.com/SpaceTimee), [xiaopangju](https://github.com/xiaopangju), Yisme |
|
||||
| <img src='https://flagcdn.com/tw.svg' width=20> Traditional Chinese (Taiwan) | `zh_TW` | Aaron Liu, [CnYeSheng](https://github.com/CnYeSheng), Cololi, [enKl03B](https://github.com/enKl03B), [Henryliu880922](https://github.com/Henryliu880922), [MINAX2U](https://github.com/MINAX2U), [StarsShine11904](https://github.com/StarsShine11904), [yrctw](https://github.com/yrctw) |
|
||||
|
||||
## Maintaining Translation Data
|
||||
|
||||
The tables in this document are generated from the checked-in translation metadata. Completion is computed against the active English keys in `lang_en.json`; keys below the legacy boundary marker are excluded from the percentage.
|
||||
|
||||
To refresh translated percentages, contributor metadata, and this document after locale changes, run:
|
||||
|
||||
```powershell
|
||||
pwsh ./scripts/translation/Sync-TranslationMetadata.ps1 -AllLanguages -UpdateTranslationDoc
|
||||
```
|
||||
|
||||
To inspect the current status without modifying files, run:
|
||||
|
||||
```powershell
|
||||
pwsh ./scripts/translation/Get-TranslationStatus.ps1 -OutputFormat Markdown -OnlyIncomplete
|
||||
```
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
; Script generated by the Inno Setup Script Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppVersion "3.3.7"
|
||||
#define MyAppName "UniGetUI"
|
||||
#define MyAppPublisher "Devolutions Inc."
|
||||
#define MyAppURL "https://github.com/Devolutions/UniGetUI"
|
||||
#define MyAppExeName "UniGetUI.exe"
|
||||
#ifndef InstallerCompression
|
||||
#define InstallerCompression "lzma"
|
||||
#endif
|
||||
|
||||
#define public Dependency_Path_NetCoreCheck "InstallerExtras\"
|
||||
#include "InstallerExtras\CodeDependencies.iss"
|
||||
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
UninstallDisplayName="UniGetUI"
|
||||
AppId={{889610CC-4337-4BDB-AC3B-4F21806C0BDE}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppVerName={#MyAppName} {#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL="https://devolutions.net/unigetui/"
|
||||
AppSupportURL={#MyAppURL}
|
||||
AppUpdatesURL={#MyAppURL}
|
||||
VersionInfoVersion=3.3.7.0
|
||||
VersionInfoProductVersion=3.3.7.0
|
||||
DefaultDirName="{autopf64}\UniGetUI"
|
||||
DisableProgramGroupPage=yes
|
||||
DisableDirPage=no
|
||||
DirExistsWarning=no
|
||||
; Force-close any process holding files we overwrite (backstop for the kill in PrepareToInstall).
|
||||
CloseApplications=force
|
||||
CloseApplicationsFilter=*.exe,*.dll
|
||||
RestartApplications=no
|
||||
; Default to per-user install mode and let the dialog opt into all-users installs when needed.
|
||||
PrivilegesRequired=lowest
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
OutputBaseFilename=UniGetUI Installer
|
||||
OutputDir=.
|
||||
; Comment line below to disable digital signature of installer
|
||||
SignTool=azsign
|
||||
|
||||
SignedUninstaller=yes
|
||||
SignedUninstallerDir=InstallerExtras\
|
||||
MinVersion=10.0
|
||||
SetupIconFile=src\SharedAssets\Assets\Images\icon.ico
|
||||
UninstallDisplayIcon={app}\UniGetUI.exe
|
||||
Compression={#InstallerCompression}
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern dynamic
|
||||
WizardImageFile=InstallerExtras\installer-banner.png
|
||||
WizardImageFileDynamicDark=InstallerExtras\installer-banner.png
|
||||
WizardSmallImageFile=InstallerExtras\unigetui-256.png
|
||||
WizardSmallImageFileDynamicDark=InstallerExtras\unigetui-256.png
|
||||
DisableWelcomePage=no
|
||||
AllowUNCPath=no
|
||||
UsePreviousTasks=yes
|
||||
UsePreviousPrivileges=yes
|
||||
UsePreviousAppDir=yes
|
||||
ChangesEnvironment=yes
|
||||
RestartIfNeededByRun=no
|
||||
Uninstallable=WizardIsTaskSelected('regularinstall')
|
||||
AppModifyPath="{app}\UniGetUI.Installer.exe" /silent /NoDeployInstaller
|
||||
|
||||
|
||||
[Languages]
|
||||
Name: "English"; MessagesFile: "compiler:Default.isl"
|
||||
Name: "Armenian"; MessagesFile: "compiler:Languages\Armenian.isl"
|
||||
Name: "BrazilianPortuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl"
|
||||
Name: "Catalan"; MessagesFile: "compiler:Languages\Catalan.isl"
|
||||
Name: "Corsican"; MessagesFile: "compiler:Languages\Corsican.isl"
|
||||
Name: "Czech"; MessagesFile: "compiler:Languages\Czech.isl"
|
||||
Name: "Danish"; MessagesFile: "compiler:Languages\Danish.isl"
|
||||
Name: "Dutch"; MessagesFile: "compiler:Languages\Dutch.isl"
|
||||
Name: "Finnish"; MessagesFile: "compiler:Languages\Finnish.isl"
|
||||
Name: "French"; MessagesFile: "compiler:Languages\French.isl"
|
||||
Name: "German"; MessagesFile: "compiler:Languages\German.isl"
|
||||
Name: "Hebrew"; MessagesFile: "compiler:Languages\Hebrew.isl"
|
||||
;Name: "Icelandic"; MessagesFile: "compiler:Languages\Icelandic.isl"
|
||||
Name: "Italian"; MessagesFile: "compiler:Languages\Italian.isl"
|
||||
Name: "Japanese"; MessagesFile: "compiler:Languages\Japanese.isl"
|
||||
Name: "Korean"; MessagesFile: "compiler:Languages\Korean.isl"
|
||||
Name: "Norwegian"; MessagesFile: "compiler:Languages\Norwegian.isl"
|
||||
Name: "Polish"; MessagesFile: "compiler:Languages\Polish.isl"
|
||||
Name: "Portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl"
|
||||
Name: "Russian"; MessagesFile: "compiler:Languages\Russian.isl"
|
||||
Name: "Slovenian"; MessagesFile: "compiler:Languages\Slovenian.isl"
|
||||
Name: "Spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
|
||||
Name: "Turkish"; MessagesFile: "compiler:Languages\Turkish.isl"
|
||||
Name: "Ukrainian"; MessagesFile: "compiler:Languages\Ukrainian.isl"
|
||||
|
||||
; Include installer's messages
|
||||
#include "InstallerExtras\CustomMessages.iss"
|
||||
|
||||
[Code]
|
||||
var
|
||||
PreserveAutostartDisabled: Boolean;
|
||||
|
||||
// StartupApproved stores the on/off state in the first byte's low bit (03 = off).
|
||||
function IsAutostartDisabledByUser: Boolean;
|
||||
var
|
||||
Data: AnsiString;
|
||||
begin
|
||||
Result := False;
|
||||
if RegQueryBinaryValue(HKEY_CURRENT_USER,
|
||||
'Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run',
|
||||
'WingetUI', Data) then
|
||||
Result := (Length(Data) >= 1) and ((Ord(Data[1]) and 1) = 1);
|
||||
end;
|
||||
|
||||
procedure InitializeWizard;
|
||||
begin
|
||||
WizardForm.Bevel.Visible := False;
|
||||
WizardForm.Bevel1.Visible := True;
|
||||
end;
|
||||
|
||||
// Kills all instances of an image and loops until none remain (taskkill returns 0 while killing, 128 when none left).
|
||||
procedure TaskKillWait(FileName: String);
|
||||
var
|
||||
ResultCode, Attempts: Integer;
|
||||
begin
|
||||
Attempts := 0;
|
||||
repeat
|
||||
if not Exec('taskkill.exe', '/f /im "' + FileName + '"', '', SW_HIDE,
|
||||
ewWaitUntilTerminated, ResultCode) then
|
||||
Break;
|
||||
if ResultCode <> 0 then
|
||||
Break;
|
||||
Sleep(500);
|
||||
Attempts := Attempts + 1;
|
||||
until Attempts >= 10;
|
||||
end;
|
||||
|
||||
procedure KillRunningApps;
|
||||
begin
|
||||
TaskKillWait('WingetUI.exe');
|
||||
TaskKillWait('UniGetUI.exe');
|
||||
TaskKillWait('UniGetUI.Avalonia.exe');
|
||||
// Elevator (gsudo cache) and pinget live in {app} and lock their own files.
|
||||
TaskKillWait('UniGetUI Elevator.exe');
|
||||
TaskKillWait('pinget.exe');
|
||||
Sleep(1000); // let the OS release file handles before copying
|
||||
|
||||
end;
|
||||
|
||||
function GetCurrentProcessId: Cardinal; external 'GetCurrentProcessId@kernel32.dll stdcall';
|
||||
|
||||
function UpdateMarkerPath(): String;
|
||||
begin
|
||||
Result := ExpandConstant('{app}\.unigetui-update-in-progress');
|
||||
end;
|
||||
|
||||
// Marker holds our PID; the app blocks only while this installer runs. Name MUST match UpdateInProgressGuard.MarkerFileName.
|
||||
procedure WriteUpdateMarker;
|
||||
var
|
||||
Pid: Int64;
|
||||
begin
|
||||
ForceDirectories(ExpandConstant('{app}'));
|
||||
Pid := GetCurrentProcessId;
|
||||
SaveStringToFile(UpdateMarkerPath(), IntToStr(Pid), False);
|
||||
end;
|
||||
|
||||
procedure RemoveUpdateMarker;
|
||||
begin
|
||||
DeleteFile(UpdateMarkerPath());
|
||||
end;
|
||||
|
||||
// Runs before any file is copied: shut everything down, then mark the copy window.
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
begin
|
||||
// Capture before [Registry] rewrites the Run key, so updates keep the user's choice.
|
||||
PreserveAutostartDisabled := IsAutostartDisabledByUser;
|
||||
KillRunningApps;
|
||||
WriteUpdateMarker;
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
// Clear the marker once the copy is done, before the post-install launch.
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
begin
|
||||
if CurStep = ssPostInstall then
|
||||
RemoveUpdateMarker;
|
||||
end;
|
||||
|
||||
function CmdLineParamExists(const Value: string): Boolean;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
for I := 1 to ParamCount do
|
||||
if CompareText(ParamStr(I), Value) = 0 then
|
||||
begin
|
||||
Result := True;
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
function IsMSStoreInstall: Boolean;
|
||||
begin
|
||||
Result := CmdLineParamExists('/MSStore');
|
||||
end;
|
||||
|
||||
function ShouldInstallVCRedist: Boolean;
|
||||
begin
|
||||
Result := not (CmdLineParamExists('/NoVCRedist') or IsMSStoreInstall);
|
||||
end;
|
||||
|
||||
function ShouldInstallEdgeWebView: Boolean;
|
||||
begin
|
||||
Result := not (CmdLineParamExists('/NoEdgeWebView') or IsMSStoreInstall);
|
||||
end;
|
||||
|
||||
function ShouldLaunchAfterInstall: Boolean;
|
||||
begin
|
||||
Result := not (CmdLineParamExists('/NoAutoStart') or IsMSStoreInstall);
|
||||
end;
|
||||
|
||||
function ShouldSuppressRunOnStartup: Boolean;
|
||||
begin
|
||||
Result := CmdLineParamExists('/NoRunOnStartup') or IsMSStoreInstall or PreserveAutostartDisabled;
|
||||
end;
|
||||
|
||||
var CustomExitCode: integer;
|
||||
|
||||
procedure ExitProcess(exitCode:integer);
|
||||
external 'ExitProcess@kernel32.dll stdcall';
|
||||
|
||||
procedure DeinitializeSetup();
|
||||
begin
|
||||
RemoveUpdateMarker; // also clear on abort, before ssPostInstall
|
||||
|
||||
if (CustomExitCode <> 0) then
|
||||
begin
|
||||
DelTree(ExpandConstant('{tmp}'), True, True, True);
|
||||
ExitProcess(0);
|
||||
end;
|
||||
end;
|
||||
|
||||
function IsCharValid(Value: Char): Boolean;
|
||||
begin
|
||||
Result := Ord(Value) <= $007F;
|
||||
end;
|
||||
|
||||
function IsDirNameValid(const Value: string): Boolean;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
for I := 1 to Length(Value) do
|
||||
if not IsCharValid(Value[I]) then
|
||||
Exit;
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function NextButtonClick(CurPageID: Integer): Boolean;
|
||||
begin
|
||||
Result := True;
|
||||
|
||||
if (CurPageID = wpSelectDir) and
|
||||
not IsDirNameValid(WizardForm.DirEdit.Text) then
|
||||
begin
|
||||
Result := False;
|
||||
MsgBox('There is an invalid character in the selected install location. ' +
|
||||
'Install location cannot contain special characters. ' +
|
||||
'Please input a valid path to continue, such as '+ExpandConstant('{commonpf64}')+'\UniGetUI', mbError, MB_OK);
|
||||
end;
|
||||
end;
|
||||
|
||||
function InitializeSetup: Boolean;
|
||||
begin
|
||||
try
|
||||
if ShouldInstallVCRedist then
|
||||
begin
|
||||
Dependency_AddVC2015To2022;
|
||||
end;
|
||||
if ShouldInstallEdgeWebView then
|
||||
begin
|
||||
Dependency_AddWebView2;
|
||||
end;
|
||||
Result := True;
|
||||
except
|
||||
Result := True;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
[Tasks]
|
||||
Name: "portableinstall"; Description: "{cm:PortInst}"; GroupDescription: "{cm:InstallType}"; Flags: unchecked exclusive
|
||||
Name: "regularinstall"; Description: "{cm:RegInst}"; GroupDescription: "{cm:InstallType}"; Flags: exclusive
|
||||
Name: "regularinstall\startmenuicon"; Description: "{cm:RegStartMmenuIcon}"; GroupDescription: "{cm:ShCuts}";
|
||||
Name: "regularinstall\desktopicon"; Description: "{cm:RegDesktopIcon}"; GroupDescription: "{cm:ShCuts}";
|
||||
|
||||
[Registry]
|
||||
Root: HKCU; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "WingetUI"; ValueData: """{app}\UniGetUI.exe"" --daemon"; Flags: uninsdeletevalue noerror; Tasks: regularinstall;
|
||||
Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run"; ValueType: binary; ValueName: "WingetUI"; ValueData: "03"; Flags: uninsdeletevalue; Tasks: regularinstall; Check: ShouldSuppressRunOnStartup;
|
||||
|
||||
// Register the unigetui:// deep link
|
||||
Root: HKA; Subkey: "Software\Classes\unigetui"; ValueType: "string"; ValueData: "URL:UniGetUI Protocol"; Flags: uninsdeletekey; Tasks: regularinstall;
|
||||
Root: HKA; Subkey: "Software\Classes\unigetui"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""; Tasks: regularinstall;
|
||||
Root: HKA; Subkey: "Software\Classes\unigetui\DefaultIcon"; ValueType: "string"; ValueData: "{app}\{#MyAppExeName},0"; Tasks: regularinstall;
|
||||
Root: HKA; Subkey: "Software\Classes\unigetui\shell\open\command"; ValueType: "string"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Tasks: regularinstall;
|
||||
|
||||
// Register the .ubundle file type
|
||||
Root: HKA; Subkey: "Software\Classes\.ubundle"; ValueType: string; ValueData: "UniGetUI.PackageBundle"; Flags: uninsdeletekey; Tasks: regularinstall;
|
||||
Root: HKA; Subkey: "Software\Classes\UniGetUI.PackageBundle"; ValueType: string; ValueData: {cm:PackageBundleName}; Flags: uninsdeletekey; Tasks: regularinstall;
|
||||
Root: HKA; Subkey: "Software\Classes\UniGetUI.PackageBundle\DefaultIcon"; ValueType: string; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey; Tasks: regularinstall;
|
||||
Root: HKA; Subkey: "Software\Classes\UniGetUI.PackageBundle\shell\open\command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey; Tasks: regularinstall;
|
||||
|
||||
|
||||
[InstallDelete]
|
||||
Type: filesandordirs; Name: "{app}\Avalonia"
|
||||
Type: filesandordirs; Name: "{app}\Assets"
|
||||
Type: files; Name: "{app}\WingetUI.exe"
|
||||
Type: files; Name: "{app}\UniGetUI.Avalonia.exe"
|
||||
Type: files; Name: "{app}\*.dll"
|
||||
Type: files; Name: "{app}\*.pdb"
|
||||
Type: files; Name: "{app}\*.pri"
|
||||
Type: files; Name: "{app}\*.xbf"
|
||||
Type: files; Name: "{app}\*.json"
|
||||
|
||||
[Files]
|
||||
; Deploy installer for autorepair jobs (unless disabled)
|
||||
Source: "{srcexe}"; DestDir: "{app}"; DestName: "UniGetUI.Installer.exe"; Flags: external ignoreversion; Tasks: regularinstall; Check: not CmdLineParamExists('/NoDeployInstaller');
|
||||
; Deploy integrity tree
|
||||
Source: "unigetui_bin\IntegrityTree.json"; DestDir: "{app}"; Flags: createallsubdirs ignoreversion recursesubdirs;
|
||||
; Deploy executable files (running instances already killed in PrepareToInstall).
|
||||
Source: "unigetui_bin\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion;
|
||||
Source: "unigetui_bin\*"; DestDir: "{app}"; Flags: createallsubdirs ignoreversion recursesubdirs;
|
||||
; Make installation portable (if required)
|
||||
Source: "InstallerExtras\ForceUniGetUIPortable"; DestDir: "{app}"; Tasks: portableinstall
|
||||
|
||||
|
||||
[Icons]
|
||||
Name: "{autostartmenu}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: regularinstall\startmenuicon
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: regularinstall\desktopicon
|
||||
|
||||
[Run]
|
||||
; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File -NonInteractive ""{tmp}\EnsureWinGet.ps1"""; StatusMsg: "Ensuring WinGet is properly installed... (this may take a while)"; WorkingDir: {app}; Check: not CmdLineParamExists('/NoWinGet'); Flags: runhidden
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: runasoriginaluser nowait postinstall; Check: ShouldLaunchAfterInstall;
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--migrate-wingetui-to-unigetui"; StatusMsg: "Removing old icons...";
|
||||
|
||||
|
||||
[UninstallRun]
|
||||
; Remove WingetUI Notification registries
|
||||
; Filename: "{app}\{#MyAppExeName}"; Parameters: "--uninstall-unigetui"; Flags: skipifdoesntexist runhidden;
|
||||
Filename: {sys}\taskkill.exe; Parameters: "/f /im WingetUI.exe"; Flags: skipifdoesntexist runhidden; RunOnceId: "KillWingetUI"
|
||||
Filename: {sys}\taskkill.exe; Parameters: "/f /im UniGetUI.exe"; Flags: skipifdoesntexist runhidden; RunOnceId: "KillUniGetUI"
|
||||
Filename: {sys}\taskkill.exe; Parameters: "/f /im UniGetUI.Avalonia.exe"; Flags: skipifdoesntexist runhidden; RunOnceId: "KillUniGetUIAvalonia"
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,10 @@
|
||||
# Available configurations
|
||||
|
||||
### `unigetui-min.winget`
|
||||
Installs UniGetUI and its dependencies. WinGet and PowerShell 5 will work out of the box, but other package managers will require manual installation.
|
||||
|
||||
### `unigetui-full.winget`
|
||||
Includes everything from `unigetui-min.winget` and also installs all supported package managers (except for vcpkg and Scoop, which must be installed manually).
|
||||
|
||||
### `develop-unigetui.winget`
|
||||
Includes everything from `unigetui-full.winget`, installs required development tools, and clones the repository to your user folder.
|
||||
@@ -0,0 +1,100 @@
|
||||
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
|
||||
|
||||
metadata:
|
||||
name: UniGetUI Development Environment
|
||||
description: Sets up the development environment for UniGetUI
|
||||
author: Martí Climent
|
||||
|
||||
resources:
|
||||
# Basic dependencies
|
||||
- name: Install Microsoft Edge WebView2
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.EdgeWebView2Runtime
|
||||
source: winget
|
||||
|
||||
- name: Install Microsoft Visual C++ 2015-2022 Redistributable
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.VCRedist.2015+.x64
|
||||
source: winget
|
||||
|
||||
# Package Managers (for testing UniGetUI functionality)
|
||||
- name: Install Chocolatey
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Chocolatey.Chocolatey
|
||||
source: winget
|
||||
|
||||
- name: Install Python
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Python.Python.3.13
|
||||
source: winget
|
||||
|
||||
- name: Install PowerShell 7
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.PowerShell
|
||||
source: winget
|
||||
|
||||
- name: Install NodeJS
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: OpenJS.NodeJS
|
||||
source: winget
|
||||
|
||||
- name: Install Rust (Cargo)
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Rustlang.Rustup
|
||||
source: winget
|
||||
|
||||
# Build and deployment tools
|
||||
- name: Install Git for version control
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Git.Git
|
||||
source: winget
|
||||
|
||||
- name: Install Visual Studio 2022 Community
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.VisualStudio.2022.Community
|
||||
source: winget
|
||||
|
||||
- name: Install .NET 10 SDK
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.DotNet.SDK.10
|
||||
source: winget
|
||||
|
||||
- name: Install Windows App SDK
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.WindowsAppRuntime.1.7
|
||||
source: winget
|
||||
|
||||
- name: Install Windows SDK
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.WindowsSDK.10.0.19041
|
||||
source: winget
|
||||
|
||||
- name: Install Inno Setup (for installer creation)
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: JRSoftware.InnoSetup
|
||||
source: winget
|
||||
|
||||
- name: Install 7-Zip (for archive handling)
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: 7zip.7zip
|
||||
source: winget
|
||||
|
||||
- name: Install gsudo (sudo for Windows)
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: gerardog.gsudo
|
||||
source: winget
|
||||
@@ -0,0 +1,65 @@
|
||||
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
|
||||
|
||||
metadata:
|
||||
name: UniGetUI Complete Environment
|
||||
description: Installs all dependencies and tools for UniGetUI development and runtime
|
||||
author: Martí Climent
|
||||
|
||||
resources:
|
||||
# Basic dependencies
|
||||
- name: Install Microsoft Edge WebView2
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.EdgeWebView2Runtime
|
||||
source: winget
|
||||
|
||||
- name: Install Microsoft Visual C++ 2015-2022 Redistributable
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.VCRedist.2015+.x64
|
||||
source: winget
|
||||
|
||||
- name: Install UniGetUI
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: MartiCliment.UniGetUI
|
||||
source: winget
|
||||
|
||||
# Package Managers (for testing UniGetUI functionality)
|
||||
- name: Install Chocolatey
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Chocolatey.Chocolatey
|
||||
source: winget
|
||||
|
||||
- name: Install Python
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Python.Python.3.13
|
||||
source: winget
|
||||
|
||||
- name: Install PowerShell 7
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.PowerShell
|
||||
source: winget
|
||||
|
||||
- name: Install NodeJS
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: OpenJS.NodeJS
|
||||
source: winget
|
||||
|
||||
- name: Install Rust (Cargo)
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Rustlang.Rustup
|
||||
source: winget
|
||||
|
||||
- name: Install .NET 10 SDK
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.DotNet.SDK.10
|
||||
source: winget
|
||||
|
||||
# vcpkg bootstrap is optional; run scripts\dev-setup-optional.ps1 if needed.
|
||||
@@ -0,0 +1,26 @@
|
||||
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
|
||||
|
||||
metadata:
|
||||
name: UniGetUI Minimal Runtime Dependencies
|
||||
description: Installs the minimal runtime dependencies required for UniGetUI
|
||||
author: Martí Climent
|
||||
|
||||
resources:
|
||||
# Basic dependencies
|
||||
- name: Install Microsoft Edge WebView2
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.EdgeWebView2Runtime
|
||||
source: winget
|
||||
|
||||
- name: Install Microsoft Visual C++ 2015-2022 Redistributable
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: Microsoft.VCRedist.2015+.x64
|
||||
source: winget
|
||||
|
||||
- name: Install UniGetUI
|
||||
type: Microsoft.WinGet/Package
|
||||
properties:
|
||||
id: MartiCliment.UniGetUI
|
||||
source: winget
|
||||
@@ -0,0 +1,262 @@
|
||||
# UniGetUI command-line interface
|
||||
|
||||
This file documents the **public command-line surface** exposed by UniGetUI in the 2026 CLI redesign.
|
||||
|
||||
- For the background IPC API that powers these commands, see [IPC.md](IPC.md).
|
||||
- For developer-only Avalonia diagnostics toggles, see the project source and build props; they are intentionally not documented here as public CLI arguments.
|
||||
|
||||
## Quick start
|
||||
|
||||
```powershell
|
||||
unigetui status
|
||||
unigetui app status
|
||||
unigetui package search --manager dotnet-tool --query dotnetsay
|
||||
unigetui package install --manager dotnet-tool --id dotnetsay --version 2.1.4 --scope Global
|
||||
unigetui operation wait --id 123 --timeout 300
|
||||
```
|
||||
|
||||
## Global transport options
|
||||
|
||||
These options select how the CLI connects to the local UniGetUI automation session.
|
||||
|
||||
| Option | Meaning |
|
||||
| --- | --- |
|
||||
| `--transport {named-pipe\|tcp}` | Client-side transport override. Default is `named-pipe`. |
|
||||
| `--tcp-port <port>` | Client-side TCP port override. Used only with `tcp`. |
|
||||
| `--pipe-name <name-or-path>` | Client-side named-pipe override. On Windows this is a pipe name. On non-Windows a relative name resolves under `/tmp`, while an absolute path uses that exact Unix socket path. |
|
||||
|
||||
Related environment variables:
|
||||
|
||||
| Variable | Meaning |
|
||||
| --- | --- |
|
||||
| `UNIGETUI_IPC_API_TRANSPORT` | Same as `--transport`. |
|
||||
| `UNIGETUI_IPC_API_PORT` | Same as `--tcp-port`. |
|
||||
| `UNIGETUI_IPC_API_PIPE_NAME` | Same as `--pipe-name`. |
|
||||
|
||||
## Exit codes
|
||||
|
||||
| Code | Meaning |
|
||||
| --- | --- |
|
||||
| `0` | Success |
|
||||
| `1` | Command failed |
|
||||
| `2` | Invalid parameter |
|
||||
| `3` | IPC API unavailable |
|
||||
| `4` | Unknown automation command |
|
||||
|
||||
## Command grammar notes
|
||||
|
||||
- Command nouns accept singular or plural forms: `operation`/`operations`, `package`/`packages`, `manager`/`managers`, and so on.
|
||||
- Compatibility aliases are accepted for some flags:
|
||||
- `--id` maps to `--package-id` or `--operation-id` where appropriate
|
||||
- `--source` maps to `--package-source`
|
||||
- Boolean options use explicit values such as `--enabled true` or `--wait false`.
|
||||
- `--detach` is shorthand for asynchronous package operations (`--wait false`).
|
||||
- `--manager` uses stable manager ids, not GUI labels. Current ids: `apt`, `bun`, `cargo`, `chocolatey`, `dnf`, `dotnet-tool`, `flatpak`, `homebrew`, `npm`, `pacman`, `pip`, `pwsh`, `scoop`, `snap`, `vcpkg`, `winget`, and `winps`.
|
||||
|
||||
## Command reference
|
||||
|
||||
### Core
|
||||
|
||||
| Command | Required options | Optional options | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `status` | None | None | Returns transport, endpoint, and build information for the selected automation session. |
|
||||
| `version` | None | None | Returns the UniGetUI build number through the IPC API. |
|
||||
|
||||
### App
|
||||
|
||||
| Command | Required options | Optional options | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `app status` | None | None | Returns app/session state such as headless mode, page, and supported UI actions. |
|
||||
| `app show` | None | None | Shows and focuses the window when a GUI session exists. |
|
||||
| `app navigate` | `--page <page>` | `--manager <id>`, `--help-attachment <path>` | Valid pages include `discover`, `updates`, `installed`, `bundles`, `settings`, `managers`, `own-log`, `manager-log`, `operation-history`, `help`, `release-notes`, and `about`. |
|
||||
| `app quit` | None | None | Gracefully shuts down the selected session, including headless daemons. |
|
||||
|
||||
### Operations
|
||||
|
||||
| Command | Required options | Optional options | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `operation list` | None | None | Lists tracked live and completed operations. |
|
||||
| `operation get` | `--id <operation-id>` | None | Returns the full tracked payload for one operation. |
|
||||
| `operation output` | `--id <operation-id>` | `--tail <n>` | Reads captured output lines for one operation. |
|
||||
| `operation wait` | `--id <operation-id>` | `--timeout <seconds>`, `--delay <seconds>` | Polls until the operation reaches a terminal state. |
|
||||
| `operation cancel` | `--id <operation-id>` | None | Cancels a queued or running operation. |
|
||||
| `operation retry` | `--id <operation-id>` | `--mode <mode>` | Retry modes are defined by the operation payload. |
|
||||
| `operation reorder` | `--id <operation-id>`, `--action <run-now\|run-next\|run-last>` | None | Reorders a queued operation. |
|
||||
| `operation forget` | `--id <operation-id>` | None | Removes a finished operation from the live tracked list. |
|
||||
|
||||
### Managers
|
||||
|
||||
| Command | Required options | Optional options | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `manager list` | None | None | Lists managers and their automation-relevant capability flags. |
|
||||
| `manager maintenance` | `--manager <id>` | None | Returns maintenance metadata for one manager. |
|
||||
| `manager reload` | `--manager <id>` | None | Reloads one manager. |
|
||||
| `manager set-executable` | `--manager <id>`, `--path <path>` | None | Sets a custom executable override, then reloads the manager. |
|
||||
| `manager clear-executable` | `--manager <id>` | None | Clears the custom executable override, then reloads the manager. |
|
||||
| `manager action` | `--manager <id>`, `--action <action>` | `--confirm` | Runs a manager-specific maintenance action. |
|
||||
| `manager enable` | `--manager <id>` | None | Enables the manager. |
|
||||
| `manager disable` | `--manager <id>` | None | Disables the manager. |
|
||||
| `manager notifications enable` | `--manager <id>` | None | Enables update notifications for the manager. |
|
||||
| `manager notifications disable` | `--manager <id>` | None | Disables update notifications for the manager. |
|
||||
|
||||
### Sources
|
||||
|
||||
| Command | Required options | Optional options | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `source list` | None | `--manager <id>` | Lists sources, optionally filtered to one manager. |
|
||||
| `source add` | `--manager <id>`, `--name <source-name>` | `--url <source-url>` | Adds a source. |
|
||||
| `source remove` | `--manager <id>`, `--name <source-name>` | `--url <source-url>` | Removes a source. |
|
||||
|
||||
### Settings
|
||||
|
||||
| Command | Required options | Optional options | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `settings list` | None | None | Lists non-secure settings. |
|
||||
| `settings get` | `--key <key>` | None | Reads one non-secure setting. |
|
||||
| `settings set` | `--key <key>` | `--enabled true\|false`, `--value <text>` | Sets either the boolean or string form of a setting. |
|
||||
| `settings clear` | `--key <key>` | None | Clears a string-backed setting. |
|
||||
| `settings reset` | None | None | Resets non-secure settings. |
|
||||
| `settings secure list` | None | `--user <name>` | Lists secure settings for the current or specified user. |
|
||||
| `settings secure get` | `--key <key>` | `--user <name>` | Reads one secure setting. |
|
||||
| `settings secure set` | `--key <key>`, `--enabled true\|false` | `--user <name>` | Enables or disables one secure setting. |
|
||||
|
||||
Available keys live in:
|
||||
|
||||
- [`src/UniGetUI.Core.Settings/SettingsEngine_Names.cs`](src/UniGetUI.Core.Settings/SettingsEngine_Names.cs)
|
||||
- [`src/UniGetUI.Core.SecureSettings/SecureSettings.cs`](src/UniGetUI.Core.SecureSettings/SecureSettings.cs)
|
||||
|
||||
### Shortcuts
|
||||
|
||||
| Command | Required options | Optional options | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `shortcut list` | None | None | Lists tracked desktop shortcuts and stored keep/delete verdicts. |
|
||||
| `shortcut set` | `--path <path>`, `--status <keep\|delete>` | None | Marks a shortcut to keep or delete. |
|
||||
| `shortcut reset` | `--path <path>` | None | Clears the stored verdict for one shortcut. |
|
||||
| `shortcut reset-all` | None | None | Clears all stored shortcut verdicts. |
|
||||
|
||||
### Logs
|
||||
|
||||
| Command | Required options | Optional options | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `log app` | None | `--level <n>` | Returns structured application log entries. |
|
||||
| `log operations` | None | None | Returns persisted operation history. |
|
||||
| `log manager` | None | `--manager <id>`, `--verbose` | Returns manager task logs. |
|
||||
|
||||
### Backups
|
||||
|
||||
| Command | Required options | Optional options | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `backup status` | None | None | Returns backup settings and cloud-auth state. |
|
||||
| `backup local create` | None | None | Creates a local backup bundle. |
|
||||
| `backup github login start` | None | `--launch-browser` | Starts the GitHub device flow. |
|
||||
| `backup github login complete` | None | None | Completes the pending device flow. |
|
||||
| `backup github logout` | None | None | Clears the stored GitHub auth token. |
|
||||
| `backup cloud list` | None | None | Lists cloud backups in the authenticated GitHub backup store. |
|
||||
| `backup cloud create` | None | None | Uploads the current backup to cloud storage. |
|
||||
| `backup cloud download` | `--key <name>` | None | Downloads one cloud backup as bundle content. |
|
||||
| `backup cloud restore` | `--key <name>` | `--append` | Imports one cloud backup into the current in-memory bundle. |
|
||||
|
||||
### Bundles
|
||||
|
||||
| Command | Required options | Optional options | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `bundle get` | None | None | Returns the current in-memory bundle. |
|
||||
| `bundle reset` | None | None | Clears the current in-memory bundle. |
|
||||
| `bundle import` | None | `--path <path>`, `--content <text>`, `--format <ubundle\|json\|yaml\|xml>`, `--append` | Imports bundle content from a file or raw content. |
|
||||
| `bundle export` | None | `--path <path>` | Exports the current bundle, optionally to disk. |
|
||||
| `bundle add` | `--id <package-id>` | `--manager <id>`, `--source <source>`, `--version <version>`, `--scope <scope>`, `--pre-release`, `--selection <search\|installed\|updates\|auto>` | Resolves a package and adds it to the bundle. |
|
||||
| `bundle remove` | `--id <package-id>` | `--manager <id>`, `--source <source>`, `--version <version>`, `--scope <scope>`, `--pre-release`, `--selection <mode>` | Removes matching package entries from the bundle. |
|
||||
| `bundle install` | None | `--include-installed true\|false`, `--elevated true\|false`, `--interactive true\|false`, `--skip-hash true\|false` | Installs the bundle through UniGetUI’s shared operation pipeline. |
|
||||
|
||||
### Packages
|
||||
|
||||
| Command | Required options | Optional options | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `package search` | `--query <text>` | `--manager <id>`, `--max-results <n>` | Searches packages. |
|
||||
| `package details` | `--id <package-id>` | `--manager <id>`, `--source <source>` | Returns the package details payload. |
|
||||
| `package versions` | `--id <package-id>` | `--manager <id>`, `--source <source>` | Returns installable versions when supported by the manager. |
|
||||
| `package installed` | None | `--manager <id>` | Lists installed packages. |
|
||||
| `package updates` | None | `--manager <id>` | Lists available updates. |
|
||||
| `package install` | `--id <package-id>` | `--manager <id>`, `--source <source>`, `--version <version>`, `--scope <scope>`, `--pre-release`, `--elevated true\|false`, `--interactive true\|false`, `--skip-hash true\|false`, `--architecture <value>`, `--location <path>`, `--wait true\|false`, `--detach` | Installs a package. Async mode returns an operation id immediately. |
|
||||
| `package download` | `--id <package-id>` | `--manager <id>`, `--source <source>`, `--version <version>`, `--scope <scope>`, `--wait true\|false`, `--detach`, `--output <path>` | Downloads a package artifact. |
|
||||
| `package reinstall` | `--id <package-id>` | Same options as `package install` | Re-runs installation for an installed package. |
|
||||
| `package repair` | `--id <package-id>` | Same options as `package install`, plus `--remove-data true\|false` | Uninstalls then reinstalls the package. |
|
||||
| `package update` | `--id <package-id>` | Same options as `package install` | Updates one package. |
|
||||
| `package uninstall` | `--id <package-id>` | `--manager <id>`, `--source <source>`, `--scope <scope>`, `--remove-data true\|false`, `--elevated true\|false`, `--interactive true\|false`, `--wait true\|false`, `--detach` | Uninstalls a package. |
|
||||
| `package show` | `--id <package-id>`, `--source <source>` | None | Opens the package details UI flow. |
|
||||
| `package ignored list` | None | None | Lists ignored-update rules tracked by UniGetUI. |
|
||||
| `package ignored add` | `--id <package-id>` | `--manager <id>`, `--version <version>`, `--source <source>` | Adds an ignored-update rule. |
|
||||
| `package ignored remove` | `--id <package-id>` | `--manager <id>`, `--version <version>`, `--source <source>` | Removes an ignored-update rule. |
|
||||
| `package update-all` | None | None | Queues updates for all currently upgradable packages. |
|
||||
| `package update-manager` | `--manager <id>` | None | Queues updates for all upgradable packages handled by one manager. |
|
||||
|
||||
## Headless behavior
|
||||
|
||||
When UniGetUI is started with `--headless`, it exposes the same automation API without opening a window.
|
||||
|
||||
| Command | Headless behavior |
|
||||
| --- | --- |
|
||||
| `status`, `app status`, `app quit` | Fully supported. |
|
||||
| `app show` | Fails with “the current UniGetUI session is running headless and has no window to show.” |
|
||||
| `app navigate` | Fails with “the current UniGetUI session is running headless and cannot navigate UI pages.” |
|
||||
| `package show` | UI-oriented; may fail or be meaningless in pure headless sessions. |
|
||||
| `package update-all`, `package update-manager` | Require GUI-side upgrade handlers. Headless sessions may return “cannot update all packages” or “cannot update manager packages.” |
|
||||
|
||||
## Headless IPC options
|
||||
|
||||
When UniGetUI is started with `--headless`, these options control the IPC listener:
|
||||
|
||||
| Option | Meaning |
|
||||
| --- | --- |
|
||||
| `--ipc-api-transport {named-pipe\|tcp}` | Selects the server-side IPC transport. Default is `named-pipe`. |
|
||||
| `--ipc-api-port <port>` | Overrides the TCP port when TCP transport is selected. |
|
||||
| `--ipc-api-pipe-name <name-or-path>` | Overrides the server-side pipe name or Unix socket path. |
|
||||
|
||||
## Other application startup parameters
|
||||
|
||||
These parameters are accepted by the app executables in addition to the automation verb tree.
|
||||
|
||||
| Parameter | Meaning | Notes |
|
||||
| --- | --- | --- |
|
||||
| `--daemon` | Starts UniGetUI minimized to the notification area. | Requires the corresponding startup setting. |
|
||||
| `--welcome` | Opens the setup wizard. | Historical compatibility flag. |
|
||||
| `--updateapps` | Forces automatic installation of available updates. | Historical compatibility flag. |
|
||||
| `--report-all-errors` | Opens the error report page for any crash while loading. | Troubleshooting flag. |
|
||||
| `--uninstall-unigetui` | Unregisters UniGetUI from the notification panel and quits. | Historical; only valid for specific old versions. |
|
||||
| `--migrate-wingetui-to-unigetui` | Migrates legacy WingetUI data and shortcuts, then quits. | Migration helper. |
|
||||
| `--help` / `-h` | Prints CLI help. | For the direct verb-based CLI. |
|
||||
| `--import-settings <file>` | Imports settings from a JSON file. | Existing settings are replaced. |
|
||||
| `--export-settings <file>` | Exports settings to a JSON file. | Creates or overwrites the file. |
|
||||
| `--enable-setting <key>` / `--disable-setting <key>` | Toggles one boolean setting. | Legacy setting flags. |
|
||||
| `--set-setting-value <key> <value>` | Sets one string-backed setting. | Legacy setting flag. |
|
||||
| `--no-corrupt-dialog` | Shows the verbose crash report instead of the simplified dialog. | Troubleshooting flag. |
|
||||
| `--enable-secure-setting <key>` / `--disable-secure-setting <key>` | Toggles one secure setting for the current user. | May require elevation. |
|
||||
| `--enable-secure-setting-for-user <user> <key>` / `--disable-secure-setting-for-user <user> <key>` | Toggles one secure setting for a specified user. | May require elevation. |
|
||||
| `<bundle-file>` | Loads a valid bundle file into the Package Bundles page. | Supported extensions include `.ubundle`, `.json`, `.yaml`, and `.xml`. |
|
||||
|
||||
## Deep links
|
||||
|
||||
UniGetUI also accepts the following `unigetui://` links:
|
||||
|
||||
| Deep link | Meaning |
|
||||
| --- | --- |
|
||||
| `unigetui://showPackage?id={id}&managerName={manager}&sourceName={source}` | Opens package details for the specified package. |
|
||||
| `unigetui://showUniGetUI` | Shows UniGetUI and brings the window to the front. |
|
||||
| `unigetui://showDiscoverPage` | Opens the Discover page. |
|
||||
| `unigetui://showUpdatesPage` | Opens the Updates page. |
|
||||
| `unigetui://showInstalledPage` | Opens the Installed page. |
|
||||
|
||||
## Installer parameters
|
||||
|
||||
The installer is Inno Setup based. It supports the standard [Inno Setup command-line parameters](https://jrsoftware.org/ishelp/index.php?topic=setupcmdline) plus these UniGetUI-specific switches:
|
||||
|
||||
| Parameter | Meaning |
|
||||
| --- | --- |
|
||||
| `/NoAutoStart` | Do not launch UniGetUI after installation. |
|
||||
| `/NoRunOnStartup` | Do not register UniGetUI to start minimized at login. |
|
||||
| `/NoVCRedist` | Skip installation of the MSVC x64 runtime. |
|
||||
| `/NoEdgeWebView` | Skip installation of the Microsoft Edge WebView runtime. |
|
||||
| `/NoChocolatey` | Deprecated no-op kept for compatibility. |
|
||||
| `/EnableSystemChocolatey` | Deprecated no-op kept for compatibility. |
|
||||
| `/NoWinGet` | Do not install WinGet and Microsoft.WinGet.Client if they are missing. |
|
||||
| `/MSStore` | Microsoft Store install mode: skip the MSVC and WebView2 dependency installers, do not launch UniGetUI after installation, and disable startup at login. Use with `/CURRENTUSER` to select user-local scope. |
|
||||
@@ -0,0 +1,397 @@
|
||||
# UniGetUI background IPC API
|
||||
|
||||
This file documents the **local automation API** used by the UniGetUI CLI.
|
||||
|
||||
- For the public command-line interface built on top of this API, see [CLI.md](CLI.md).
|
||||
- This API is designed for **local automation**, not for remote exposure.
|
||||
|
||||
## Overview
|
||||
|
||||
UniGetUI exposes a local HTTP API over one of two transports:
|
||||
|
||||
- **Named-pipe transport** (default)
|
||||
- Windows: Windows named pipe
|
||||
- Non-Windows: Unix domain socket
|
||||
- **TCP transport** (optional)
|
||||
- Localhost only
|
||||
|
||||
All endpoints live under `/uniget/v1/...`.
|
||||
|
||||
## Transport defaults
|
||||
|
||||
| Setting | Value |
|
||||
| --- | --- |
|
||||
| Default transport | `named-pipe` |
|
||||
| Default TCP port | `7058` |
|
||||
| Default pipe name | `UniGetUI.IPC` |
|
||||
| Default Unix socket directory | `/tmp` |
|
||||
|
||||
On non-Windows, a relative named-pipe name such as `UniGetUI.IPC` resolves to:
|
||||
|
||||
```text
|
||||
/tmp/UniGetUI.IPC
|
||||
```
|
||||
|
||||
An absolute path may also be supplied on non-Windows. On Windows, absolute pipe paths are rejected and UniGetUI falls back to the default pipe name.
|
||||
|
||||
## Server-side configuration
|
||||
|
||||
These options are read when UniGetUI starts its IPC API server.
|
||||
|
||||
| Argument | Environment variable | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `--ipc-api-transport {named-pipe\|tcp}` | `UNIGETUI_IPC_API_TRANSPORT` | Selects the server transport. |
|
||||
| `--ipc-api-port <port>` | `UNIGETUI_IPC_API_PORT` | Selects the TCP port when TCP is enabled. |
|
||||
| `--ipc-api-pipe-name <name-or-path>` | `UNIGETUI_IPC_API_PIPE_NAME` | Selects the pipe name or Unix socket path when named-pipe transport is enabled. |
|
||||
|
||||
## Client-side configuration
|
||||
|
||||
These options are read by the CLI and `IpcClient`.
|
||||
|
||||
| Argument | Environment variable | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `--transport {named-pipe\|tcp}` | `UNIGETUI_IPC_API_TRANSPORT` | Explicit client-side transport override. |
|
||||
| `--tcp-port <port>` | `UNIGETUI_IPC_API_PORT` | Explicit client-side TCP port override. |
|
||||
| `--pipe-name <name-or-path>` | `UNIGETUI_IPC_API_PIPE_NAME` | Explicit client-side pipe name or Unix socket override. |
|
||||
|
||||
## Session discovery
|
||||
|
||||
When the client does **not** receive an explicit transport override:
|
||||
|
||||
1. UniGetUI loads persisted endpoint registrations from the user configuration directory.
|
||||
2. Registrations are ordered with this preference:
|
||||
1. headless sessions first
|
||||
2. newest persisted session first
|
||||
3. The client probes for a live session and uses its persisted token automatically.
|
||||
|
||||
When the client **does** receive an explicit override:
|
||||
|
||||
- it connects to that transport choice instead of auto-selecting the newest session
|
||||
- it waits up to 5 seconds for a matching persisted token to appear
|
||||
|
||||
## Authentication
|
||||
|
||||
| Endpoint | Auth |
|
||||
| --- | --- |
|
||||
| `GET /uniget/v1/status` | No token required |
|
||||
| All other `/uniget/v1/*` endpoints | `token` query parameter required |
|
||||
|
||||
Authentication details:
|
||||
|
||||
- UniGetUI generates a per-session token at API startup.
|
||||
- That token is persisted with the endpoint registration metadata.
|
||||
- `IpcClient` automatically appends `token=<value>` to authenticated requests.
|
||||
|
||||
## Security notes
|
||||
|
||||
- The default design is **local-only automation**.
|
||||
- TCP mode binds to `localhost`, not all interfaces.
|
||||
- On non-Windows named-pipe transport, UniGetUI applies Unix socket mode:
|
||||
|
||||
```text
|
||||
user-read + user-write
|
||||
```
|
||||
|
||||
That is effectively `0600`-style same-user access on the socket file.
|
||||
|
||||
- On Windows named-pipe transport, UniGetUI uses Kestrel named-pipe hosting and does not expose a filesystem socket path.
|
||||
|
||||
## Error model
|
||||
|
||||
| Condition | Result |
|
||||
| --- | --- |
|
||||
| Missing or invalid token | HTTP `401` |
|
||||
| Invalid query/body arguments | HTTP `400` with plain-text error message |
|
||||
| Success | JSON response with camelCase property names |
|
||||
|
||||
Most successful command endpoints return either:
|
||||
|
||||
- a domain object wrapped in a `status: "success"` envelope, or
|
||||
- a command/result JSON envelope, or
|
||||
- another typed JSON payload documented by its fields rather than its CLR type name
|
||||
|
||||
## Request conventions
|
||||
|
||||
### Query-string endpoints
|
||||
|
||||
Most endpoints use query parameters, including:
|
||||
|
||||
- operations
|
||||
- app navigation
|
||||
- sources
|
||||
- settings
|
||||
- secure settings
|
||||
- shortcuts
|
||||
- logs
|
||||
- package search/details/versions/installed/updates
|
||||
- package actions
|
||||
|
||||
### JSON-body endpoints
|
||||
|
||||
These endpoint families consume JSON bodies:
|
||||
|
||||
| Endpoint family | Request shape |
|
||||
| --- | --- |
|
||||
| manager maintenance actions | manager maintenance request body |
|
||||
| GitHub device-flow start | GitHub device-flow start request body |
|
||||
| cloud backup download/restore | cloud backup request body |
|
||||
| bundle import | bundle import request body |
|
||||
| bundle export | bundle export request body |
|
||||
| bundle add/remove | bundle package request body |
|
||||
| bundle install | bundle install request body |
|
||||
|
||||
### JSON body field reference
|
||||
|
||||
All request bodies use **camelCase** JSON.
|
||||
|
||||
#### Manager maintenance request body
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `managerName` | string | Required stable manager id |
|
||||
| `action` | string | Manager action name for `/action` |
|
||||
| `path` | string | Custom executable path for `/executable/set` |
|
||||
| `confirm` | boolean | Confirmation flag for destructive actions |
|
||||
|
||||
#### GitHub device-flow start request body
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `launchBrowser` | boolean | Whether UniGetUI should try to open the verification URL automatically |
|
||||
|
||||
#### Cloud backup request body
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `key` | string | Backup identifier |
|
||||
| `append` | boolean | Append instead of replace when restoring/importing |
|
||||
|
||||
#### Bundle import request body
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `path` | string | Source file path |
|
||||
| `content` | string | Raw bundle content |
|
||||
| `format` | string | Bundle format such as `ubundle`, `json`, `yaml`, or `xml` |
|
||||
| `append` | boolean | Append imported items to the current bundle |
|
||||
|
||||
#### Bundle export request body
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `path` | string | Optional output path |
|
||||
|
||||
#### Bundle package request body
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `packageId` | string | Package identifier |
|
||||
| `managerName` | string | Stable manager id |
|
||||
| `packageSource` | string | Source/feed name |
|
||||
| `version` | string | Requested version |
|
||||
| `scope` | string | Requested scope |
|
||||
| `preRelease` | boolean | Include prerelease package metadata |
|
||||
| `selection` | string | Bundle selection mode |
|
||||
|
||||
#### Bundle install request body
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `includeInstalled` | boolean | Whether already-installed packages should still be processed |
|
||||
| `elevated` | boolean | Request elevated execution |
|
||||
| `interactive` | boolean | Request interactive execution |
|
||||
| `skipHash` | boolean | Skip hash validation when supported |
|
||||
|
||||
## Shared parameter sets
|
||||
|
||||
### Package action query parameters
|
||||
|
||||
These keys are used by package-related endpoints such as install, update, uninstall, details, versions, ignored updates, and download.
|
||||
|
||||
| Query key | Meaning |
|
||||
| --- | --- |
|
||||
| `packageId` | Package identifier |
|
||||
| `manager` | Stable manager id |
|
||||
| `packageSource` | Source/feed name |
|
||||
| `version` | Requested version |
|
||||
| `scope` | Install scope |
|
||||
| `preRelease` | Boolean |
|
||||
| `elevated` | Boolean |
|
||||
| `interactive` | Boolean |
|
||||
| `skipHash` | Boolean |
|
||||
| `removeData` | Boolean |
|
||||
| `wait` | Boolean |
|
||||
| `architecture` | Architecture override |
|
||||
| `location` | Install location override |
|
||||
| `outputPath` | Download output path |
|
||||
|
||||
### App navigation query parameters
|
||||
|
||||
| Query key | Meaning |
|
||||
| --- | --- |
|
||||
| `page` | Target page name |
|
||||
| `manager` | Optional manager context |
|
||||
| `helpAttachment` | Optional help-page attachment |
|
||||
|
||||
### Operation query parameters
|
||||
|
||||
| Query key | Meaning |
|
||||
| --- | --- |
|
||||
| `tailLines` | Used by `GET /uniget/v1/operations/{operationId}/output` |
|
||||
| `mode` | Retry mode for `POST /uniget/v1/operations/{operationId}/retry` |
|
||||
| `action` | Queue action for `POST /uniget/v1/operations/{operationId}/reorder` |
|
||||
|
||||
## Endpoint reference
|
||||
|
||||
### Session and app
|
||||
|
||||
| Method | Path | Auth | Parameters/body | CLI equivalent | Notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `GET` | `/uniget/v1/status` | No | None | `status`, `version` | Returns `running`, `transport`, `tcpPort`, `namedPipeName`, `namedPipePath`, `baseAddress`, `version`, and `buildNumber`. |
|
||||
| `GET` | `/uniget/v1/app` | Yes | None | `app status` | Returns app/headless/window state. |
|
||||
| `POST` | `/uniget/v1/app/show` | Yes | None | `app show` | UI-only in practice. |
|
||||
| `POST` | `/uniget/v1/app/navigate` | Yes | Query: `page`, optional `manager`, optional `helpAttachment` | `app navigate` | UI-only in practice. |
|
||||
| `POST` | `/uniget/v1/app/quit` | Yes | None | `app quit` | Shuts down the selected session. |
|
||||
|
||||
### Operations
|
||||
|
||||
| Method | Path | Auth | Parameters/body | CLI equivalent |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `GET` | `/uniget/v1/operations` | Yes | None | `operation list` |
|
||||
| `GET` | `/uniget/v1/operations/{operationId}` | Yes | Route: `operationId` | `operation get` |
|
||||
| `GET` | `/uniget/v1/operations/{operationId}/output` | Yes | Route: `operationId`, optional query `tailLines` | `operation output` |
|
||||
| `POST` | `/uniget/v1/operations/{operationId}/cancel` | Yes | Route: `operationId` | `operation cancel` |
|
||||
| `POST` | `/uniget/v1/operations/{operationId}/retry` | Yes | Route: `operationId`, optional query `mode` | `operation retry` |
|
||||
| `POST` | `/uniget/v1/operations/{operationId}/reorder` | Yes | Route: `operationId`, query `action` | `operation reorder` |
|
||||
| `POST` | `/uniget/v1/operations/{operationId}/forget` | Yes | Route: `operationId` | `operation forget` |
|
||||
|
||||
### Managers
|
||||
|
||||
| Method | Path | Auth | Parameters/body | CLI equivalent |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `GET` | `/uniget/v1/managers` | Yes | None | `manager list` |
|
||||
| `GET` | `/uniget/v1/managers/maintenance` | Yes | Query `manager` | `manager maintenance` |
|
||||
| `POST` | `/uniget/v1/managers/maintenance/reload` | Yes | JSON body: manager maintenance request body | `manager reload` |
|
||||
| `POST` | `/uniget/v1/managers/maintenance/executable/set` | Yes | JSON body: manager maintenance request body | `manager set-executable` |
|
||||
| `POST` | `/uniget/v1/managers/maintenance/executable/clear` | Yes | JSON body: manager maintenance request body | `manager clear-executable` |
|
||||
| `POST` | `/uniget/v1/managers/maintenance/action` | Yes | JSON body: manager maintenance request body | `manager action` |
|
||||
| `POST` | `/uniget/v1/managers/set-enabled` | Yes | Query `manager`, `enabled` | `manager enable`, `manager disable` |
|
||||
| `POST` | `/uniget/v1/managers/set-update-notifications` | Yes | Query `manager`, `enabled` | `manager notifications enable`, `manager notifications disable` |
|
||||
|
||||
### Sources
|
||||
|
||||
| Method | Path | Auth | Parameters/body | CLI equivalent |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `GET` | `/uniget/v1/sources` | Yes | Optional query `manager` | `source list` |
|
||||
| `POST` | `/uniget/v1/sources/add` | Yes | Query `manager`, `name`, optional `url` | `source add` |
|
||||
| `POST` | `/uniget/v1/sources/remove` | Yes | Query `manager`, `name`, optional `url` | `source remove` |
|
||||
|
||||
### Settings
|
||||
|
||||
| Method | Path | Auth | Parameters/body | CLI equivalent |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `GET` | `/uniget/v1/settings` | Yes | None | `settings list` |
|
||||
| `GET` | `/uniget/v1/settings/item` | Yes | Query `key` | `settings get` |
|
||||
| `POST` | `/uniget/v1/settings/set` | Yes | Query `key`, optional `enabled`, optional `value` | `settings set` |
|
||||
| `POST` | `/uniget/v1/settings/clear` | Yes | Query `key` | `settings clear` |
|
||||
| `POST` | `/uniget/v1/settings/reset` | Yes | None | `settings reset` |
|
||||
|
||||
### Secure settings
|
||||
|
||||
| Method | Path | Auth | Parameters/body | CLI equivalent |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `GET` | `/uniget/v1/secure-settings` | Yes | Optional query `user` | `settings secure list` |
|
||||
| `GET` | `/uniget/v1/secure-settings/item` | Yes | Query `key`, optional `user` | `settings secure get` |
|
||||
| `POST` | `/uniget/v1/secure-settings/set` | Yes | Query `key`, `enabled`, optional `user` | `settings secure set` |
|
||||
|
||||
### Desktop shortcuts
|
||||
|
||||
| Method | Path | Auth | Parameters/body | CLI equivalent |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `GET` | `/uniget/v1/desktop-shortcuts` | Yes | None | `shortcut list` |
|
||||
| `POST` | `/uniget/v1/desktop-shortcuts/set` | Yes | Query `path`, `status` | `shortcut set` |
|
||||
| `POST` | `/uniget/v1/desktop-shortcuts/reset` | Yes | Query `path` | `shortcut reset` |
|
||||
| `POST` | `/uniget/v1/desktop-shortcuts/reset-all` | Yes | None | `shortcut reset-all` |
|
||||
|
||||
### Logs
|
||||
|
||||
| Method | Path | Auth | Parameters/body | CLI equivalent |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `GET` | `/uniget/v1/logs/app` | Yes | Optional query `level` | `log app` |
|
||||
| `GET` | `/uniget/v1/logs/history` | Yes | None | `log operations` |
|
||||
| `GET` | `/uniget/v1/logs/manager` | Yes | Optional query `manager`, optional query `verbose` | `log manager` |
|
||||
|
||||
### Backups
|
||||
|
||||
| Method | Path | Auth | Parameters/body | CLI equivalent | Notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `GET` | `/uniget/v1/backups/status` | Yes | None | `backup status` | Includes local backup settings and GitHub auth state. |
|
||||
| `POST` | `/uniget/v1/backups/local/create` | Yes | None | `backup local create` | Creates a local backup bundle. |
|
||||
| `POST` | `/uniget/v1/backups/github/sign-in/start` | Yes | JSON body: GitHub device-flow start request body | `backup github login start` | Starts GitHub device flow. |
|
||||
| `POST` | `/uniget/v1/backups/github/sign-in/complete` | Yes | None | `backup github login complete` | Completes device flow. |
|
||||
| `POST` | `/uniget/v1/backups/github/sign-out` | Yes | None | `backup github logout` | Signs out of GitHub backup integration. |
|
||||
| `GET` | `/uniget/v1/backups/cloud` | Yes | None | `backup cloud list` | Lists cloud backups. |
|
||||
| `POST` | `/uniget/v1/backups/cloud/create` | Yes | None | `backup cloud create` | Uploads a cloud backup. |
|
||||
| `POST` | `/uniget/v1/backups/cloud/download` | Yes | JSON body: cloud backup request body | `backup cloud download` | Downloads backup content. |
|
||||
| `POST` | `/uniget/v1/backups/cloud/restore` | Yes | JSON body: cloud backup request body | `backup cloud restore` | Restores/imports a cloud backup. |
|
||||
|
||||
### Bundles
|
||||
|
||||
| Method | Path | Auth | Parameters/body | CLI equivalent |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `GET` | `/uniget/v1/bundles` | Yes | None | `bundle get` |
|
||||
| `POST` | `/uniget/v1/bundles/reset` | Yes | None | `bundle reset` |
|
||||
| `POST` | `/uniget/v1/bundles/import` | Yes | JSON body: bundle import request body | `bundle import` |
|
||||
| `POST` | `/uniget/v1/bundles/export` | Yes | JSON body: bundle export request body | `bundle export` |
|
||||
| `POST` | `/uniget/v1/bundles/add` | Yes | JSON body: bundle package request body | `bundle add` |
|
||||
| `POST` | `/uniget/v1/bundles/remove` | Yes | JSON body: bundle package request body | `bundle remove` |
|
||||
| `POST` | `/uniget/v1/bundles/install` | Yes | JSON body: bundle install request body | `bundle install` |
|
||||
|
||||
### Packages
|
||||
|
||||
| Method | Path | Auth | Parameters/body | CLI equivalent | Notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `GET` | `/uniget/v1/packages/search` | Yes | Query `query`, optional `manager`, optional `maxResults` | `package search` | Search endpoint. |
|
||||
| `GET` | `/uniget/v1/packages/installed` | Yes | Optional query `manager` | `package installed` | Installed packages. |
|
||||
| `GET` | `/uniget/v1/packages/updates` | Yes | Optional query `manager` | `package updates` | Upgradable packages. |
|
||||
| `GET` | `/uniget/v1/packages/details` | Yes | Package action query set | `package details` | Details payload. |
|
||||
| `GET` | `/uniget/v1/packages/versions` | Yes | Package action query set | `package versions` | Installable versions. |
|
||||
| `GET` | `/uniget/v1/packages/ignored` | Yes | None | `package ignored list` | Ignored-update rules. |
|
||||
| `POST` | `/uniget/v1/packages/ignore` | Yes | Package action query set | `package ignored add` | Adds ignored-update rule. |
|
||||
| `POST` | `/uniget/v1/packages/unignore` | Yes | Package action query set | `package ignored remove` | Removes ignored-update rule. |
|
||||
| `POST` | `/uniget/v1/packages/download` | Yes | Package action query set | `package download` | Starts or performs download. |
|
||||
| `POST` | `/uniget/v1/packages/install` | Yes | Package action query set | `package install` | Starts or performs install. |
|
||||
| `POST` | `/uniget/v1/packages/reinstall` | Yes | Package action query set | `package reinstall` | Reinstalls package. |
|
||||
| `POST` | `/uniget/v1/packages/update` | Yes | Package action query set | `package update` | Updates one package. |
|
||||
| `POST` | `/uniget/v1/packages/uninstall` | Yes | Package action query set | `package uninstall` | Uninstalls package. |
|
||||
| `POST` | `/uniget/v1/packages/uninstall-then-reinstall` | Yes | Package action query set | `package repair` | Repair flow. |
|
||||
| `POST` | `/uniget/v1/packages/show` | Yes | Query `packageId`, `packageSource` | `package show` | UI-oriented package-details flow. |
|
||||
| `POST` | `/uniget/v1/packages/update-all` | Yes | None | `package update-all` | Requires `OnUpgradeAll` handler to be wired. |
|
||||
| `POST` | `/uniget/v1/packages/update-manager` | Yes | Query `manager` | `package update-manager` | Requires `OnUpgradeAllForManager` handler to be wired. |
|
||||
|
||||
## Headless-specific limitations
|
||||
|
||||
In headless sessions:
|
||||
|
||||
- `POST /uniget/v1/app/show` fails because there is no window to show.
|
||||
- `POST /uniget/v1/app/navigate` fails because there is no UI page stack to navigate.
|
||||
- `POST /uniget/v1/packages/update-all` fails unless a host wires `OnUpgradeAll`.
|
||||
- `POST /uniget/v1/packages/update-manager` fails unless a host wires `OnUpgradeAllForManager`.
|
||||
|
||||
These failures are intentional and surfaced as HTTP `400` with a descriptive message.
|
||||
|
||||
## Practical testing tip
|
||||
|
||||
If you want to inspect the IPC API manually with generic tools such as `curl`, the easiest route is to start UniGetUI in **TCP mode**:
|
||||
|
||||
```powershell
|
||||
UniGetUI.exe --headless --ipc-api-transport tcp --ipc-api-port 7058
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```powershell
|
||||
curl http://localhost:7058/uniget/v1/status
|
||||
```
|
||||
|
||||
For authenticated endpoints, you must also supply the session token as the `token` query parameter. The built-in CLI and `IpcClient` resolve that automatically.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.103",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 584 B |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 333 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 369 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72">
|
||||
<path fill="#ffffff" d="M60.729,36.795c.025-2.456-1.523-4.626-3.848-5.399l-7.112-2.37,3.55-3.55-8.027-8.027-3.616,3.615v-12.464h-11.353v12.464l-3.615-3.615-8.028,8.027,3.55,3.55-7.104,2.368c-2.332.774-3.881,2.945-3.855,5.402.016,1.495.615,2.871,1.618,3.886-1.029,1.042-1.634,2.472-1.618,4.014.016,1.494.615,2.871,1.618,3.886-1.029,1.042-1.634,2.472-1.618,4.015.026,2.456,1.627,4.596,4.01,5.331l15.898,4.766c1.569.47,3.191.708,4.821.708s3.251-.239,4.819-.708l15.938-4.777c2.349-.724,3.945-2.861,3.972-5.32.016-1.541-.587-2.969-1.613-4.01.997-1.012,1.596-2.391,1.613-3.888v-.002c.016-1.541-.587-2.969-1.613-4.01.997-1.012,1.597-2.391,1.613-3.89Z"/>
|
||||
<path fill="#009eff" d="M57.275,44.66c.01-.95-.588-1.788-1.486-2.086l-5.964-1.988,5.914-1.773c.908-.279,1.525-1.105,1.535-2.054.01-.949-.588-1.788-1.486-2.086l-12.415-4.138,5.06-5.06-3.142-3.142-7.07,7.07V12.053h-4.444v17.35l-7.07-7.07-3.142,3.142,5.06,5.06-12.411,4.137c-.901.299-1.499,1.137-1.489,2.086.01.949.626,1.775,1.548,2.059l5.901,1.769-5.96,1.987c-.901.299-1.499,1.137-1.489,2.086s.626,1.775,1.548,2.059l5.901,1.769-5.96,1.987c-.901.299-1.499,1.137-1.489,2.086s.626,1.775,1.548,2.059l15.898,4.765c1.253.375,2.541.563,3.829.563s2.576-.188,3.827-.563l15.912-4.769c.908-.28,1.525-1.105,1.535-2.054.01-.95-.588-1.788-1.486-2.086l-5.964-1.988,5.914-1.773c.908-.28,1.525-1.105,1.535-2.054ZM31.748,33.658l.976.976c.875.875,2.038,1.357,3.276,1.357s2.401-.482,3.275-1.357l.976-.976,8.962,2.987-10.52,3.153c-1.762.53-3.625.53-5.387,0l-10.519-3.153,8.962-2.987ZM32.171,43.582c1.253.375,2.541.563,3.829.563s2.576-.188,3.827-.563l3.419-1.025,5.966,1.988-10.52,3.153c-1.762.53-3.625.529-5.387,0l-10.519-3.153,5.937-1.979-.007-.02,3.454,1.035ZM49.212,52.446l-10.52,3.153c-1.762.529-3.625.529-5.387,0l-10.519-3.153,5.937-1.979-.007-.02,3.454,1.035c1.253.375,2.541.563,3.829.563s2.576-.188,3.827-.563l3.419-1.025,5.966,1.988Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72">
|
||||
<defs>
|
||||
<filter id="a" width="200%" height="200%">
|
||||
<feOffset result="offOut" in="SourceAlpha" dy="2.2"/>
|
||||
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="1.5"/>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.4 0"/>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<path fill="#ffffff" filter="url(#a)" d="M60.729,36.795c.025-2.456-1.523-4.626-3.848-5.399l-7.112-2.37,3.55-3.55-8.027-8.027-3.616,3.615v-12.464h-11.353v12.464l-3.615-3.615-8.028,8.027,3.55,3.55-7.104,2.368c-2.332.774-3.881,2.945-3.855,5.402.016,1.495.615,2.871,1.618,3.886-1.029,1.042-1.634,2.472-1.618,4.014.016,1.494.615,2.871,1.618,3.886-1.029,1.042-1.634,2.472-1.618,4.015.026,2.456,1.627,4.596,4.01,5.331l15.898,4.766c1.569.47,3.191.708,4.821.708s3.251-.239,4.819-.708l15.938-4.777c2.349-.724,3.945-2.861,3.972-5.32.016-1.541-.587-2.969-1.613-4.01.997-1.012,1.596-2.391,1.613-3.888v-.002c.016-1.541-.587-2.969-1.613-4.01.997-1.012,1.597-2.391,1.613-3.89Z"/>
|
||||
<path fill="#009eff" d="M57.275,44.66c.01-.95-.588-1.788-1.486-2.086l-5.964-1.988,5.914-1.773c.908-.279,1.525-1.105,1.535-2.054.01-.949-.588-1.788-1.486-2.086l-12.415-4.138,5.06-5.06-3.142-3.142-7.07,7.07V12.053h-4.444v17.35l-7.07-7.07-3.142,3.142,5.06,5.06-12.411,4.137c-.901.299-1.499,1.137-1.489,2.086.01.949.626,1.775,1.548,2.059l5.901,1.769-5.96,1.987c-.901.299-1.499,1.137-1.489,2.086s.626,1.775,1.548,2.059l5.901,1.769-5.96,1.987c-.901.299-1.499,1.137-1.489,2.086s.626,1.775,1.548,2.059l15.898,4.765c1.253.375,2.541.563,3.829.563s2.576-.188,3.827-.563l15.912-4.769c.908-.28,1.525-1.105,1.535-2.054.01-.95-.588-1.788-1.486-2.086l-5.964-1.988,5.914-1.773c.908-.28,1.525-1.105,1.535-2.054ZM31.748,33.658l.976.976c.875.875,2.038,1.357,3.276,1.357s2.401-.482,3.275-1.357l.976-.976,8.962,2.987-10.52,3.153c-1.762.53-3.625.53-5.387,0l-10.519-3.153,8.962-2.987ZM32.171,43.582c1.253.375,2.541.563,3.829.563s2.576-.188,3.827-.563l3.419-1.025,5.966,1.988-10.52,3.153c-1.762.53-3.625.529-5.387,0l-10.519-3.153,5.937-1.979-.007-.02,3.454,1.035ZM49.212,52.446l-10.52,3.153c-1.762.529-3.625.529-5.387,0l-10.519-3.153,5.937-1.979-.007-.02,3.454,1.035c1.253.375,2.541.563,3.829.563s2.576-.188,3.827-.563l3.419-1.025,5.966,1.988Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="11.805 11.81 48.38 48.38">
|
||||
<path fill="#009eff" d="M57.275,44.66c.01-.95-.588-1.788-1.486-2.086l-5.964-1.988,5.914-1.773c.908-.279,1.525-1.105,1.535-2.054.01-.949-.588-1.788-1.486-2.086l-12.415-4.138,5.06-5.06-3.142-3.142-7.07,7.07V12.053h-4.444v17.35l-7.07-7.07-3.142,3.142,5.06,5.06-12.411,4.137c-.901.299-1.499,1.137-1.489,2.086.01.949.626,1.775,1.548,2.059l5.901,1.769-5.96,1.987c-.901.299-1.499,1.137-1.489,2.086s.626,1.775,1.548,2.059l5.901,1.769-5.96,1.987c-.901.299-1.499,1.137-1.489,2.086s.626,1.775,1.548,2.059l15.898,4.765c1.253.375,2.541.563,3.829.563s2.576-.188,3.827-.563l15.912-4.769c.908-.28,1.525-1.105,1.535-2.054.01-.95-.588-1.788-1.486-2.086l-5.964-1.988,5.914-1.773c.908-.28,1.525-1.105,1.535-2.054ZM31.748,33.658l.976.976c.875.875,2.038,1.357,3.276,1.357s2.401-.482,3.275-1.357l.976-.976,8.962,2.987-10.52,3.153c-1.762.53-3.625.53-5.387,0l-10.519-3.153,8.962-2.987ZM32.171,43.582c1.253.375,2.541.563,3.829.563s2.576-.188,3.827-.563l3.419-1.025,5.966,1.988-10.52,3.153c-1.762.53-3.625.529-5.387,0l-10.519-3.153,5.937-1.979-.007-.02,3.454,1.035ZM49.212,52.446l-10.52,3.153c-1.762.529-3.625.529-5.387,0l-10.519-3.153,5.937-1.979-.007-.02,3.454,1.035c1.253.375,2.541.563,3.829.563s2.576-.188,3.827-.563l3.419-1.025,5.966,1.988Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |