chore: import upstream snapshot with attribution
.NET Tests / test-codebase (push) Has been cancelled
Translation Validation / validate-translations (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:01:52 +08:00
commit 643e9f9fcb
1003 changed files with 247032 additions and 0 deletions
@@ -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()
+117
View File
@@ -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 2030 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
+1
View File
@@ -0,0 +1 @@
../.agents/skills
+65
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
src/UniGetUI.PackageEngine.Managers.Chocolatey/choco-cli/** linguist-vendored
.githooks/* text eol=lf
+38
View File
@@ -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
+3
View File
@@ -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
+68
View File
@@ -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
+12
View File
@@ -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
+35
View File
@@ -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
+74
View File
@@ -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
+14
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
actions-deps:
patterns:
- "*"
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"enabledManagers": [
"nuget"
],
"dependencyDashboard": true
}
+896
View File
@@ -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
+179
View File
@@ -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
+90
View File
@@ -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}}"
+115
View File
@@ -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
+99
View File
@@ -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
+108
View File
@@ -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` |
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+13
View File
@@ -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/).
+64
View File
@@ -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.
+314
View File
@@ -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
+218
View File
@@ -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"
{
}
}
}
+20
View File
@@ -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
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+21
View File
@@ -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.
+240
View File
@@ -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).
[![Downloads](https://img.shields.io/github/downloads/Devolutions/UniGetUI/total?style=for-the-badge)](https://github.com/Devolutions/UniGetUI/releases/latest)
[![Release Version Badge](https://img.shields.io/github/v/release/Devolutions/UniGetUI?style=for-the-badge)](https://github.com/Devolutions/UniGetUI/releases)
[![Issues Badge](https://img.shields.io/github/issues/Devolutions/UniGetUI?style=for-the-badge)](https://github.com/Devolutions/UniGetUI/issues)
[![Closed Issues Badge](https://img.shields.io/github/issues-closed/Devolutions/UniGetUI?color=%238256d0&style=for-the-badge)](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.
![image](media/UniGetUI-Image.png)
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.
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Devolutions/UniGetUI/dotnet-test.yml?branch=main&style=for-the-badge&label=Tests)<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:
![GitHub Release](https://img.shields.io/github/v/release/Devolutions/UniGetUI?style=for-the-badge)
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:
![WinGet Package Version](https://img.shields.io/winget/v/Devolutions.UniGetUI?style=for-the-badge)
```cmd
winget install --exact --id Devolutions.UniGetUI --source winget
```
#### Install via Scoop:
![Scoop version](https://img.shields.io/scoop/v/unigetui?bucket=extras&style=for-the-badge)
```cmd
scoop bucket add extras
scoop install extras/unigetui
```
#### Install via Chocolatey:
![Chocolatey Version](https://img.shields.io/chocolatey/v/unigetui?style=for-the-badge)
```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.
![image](media/supported-managers.svg)
✅: 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
![image](media/UniGetUI_1.png)
![image](media/UniGetUI_2.png)
![image](media/UniGetUI_3.png)
![image](media/UniGetUI_4.png)
![image](media/UniGetUI_5.png)
![image](media/UniGetUI_6.png)
![image](media/UniGetUI_7.png)
![image](media/UniGetUI_8.png)
![image](media/UniGetUI_9.png)
## 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>
[![Contributors](https://contrib.rocks/image?repo=Devolutions/UniGetUI)](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).
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`Devolutions/UniGetUI`
- 原始仓库:https://github.com/Devolutions/UniGetUI
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+5
View File
@@ -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/).
+156
View File
@@ -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> &nbsp; Afrikaans - Afrikaans | `af` | 100% | [lang_af.json](src/Languages/lang_af.json) |
| <img src='https://flagcdn.com/sa.svg' width=20> &nbsp; Arabic - عربي‎ | `ar` | 100% | [lang_ar.json](src/Languages/lang_ar.json) |
| <img src='https://flagcdn.com/by.svg' width=20> &nbsp; Belarusian - беларуская | `be` | 100% | [lang_be.json](src/Languages/lang_be.json) |
| <img src='https://flagcdn.com/bg.svg' width=20> &nbsp; Bulgarian - български | `bg` | 100% | [lang_bg.json](src/Languages/lang_bg.json) |
| <img src='https://flagcdn.com/bd.svg' width=20> &nbsp; Bangla - বাংলা | `bn` | 100% | [lang_bn.json](src/Languages/lang_bn.json) |
| <img src='https://flagcdn.com/ad.svg' width=20> &nbsp; Catalan - Català | `ca` | 100% | [lang_ca.json](src/Languages/lang_ca.json) |
| <img src='https://flagcdn.com/cz.svg' width=20> &nbsp; Czech - Čeština | `cs` | 100% | [lang_cs.json](src/Languages/lang_cs.json) |
| <img src='https://flagcdn.com/dk.svg' width=20> &nbsp; Danish - Dansk | `da` | 99% | [lang_da.json](src/Languages/lang_da.json) |
| <img src='https://flagcdn.com/de.svg' width=20> &nbsp; German - Deutsch | `de` | 100% | [lang_de.json](src/Languages/lang_de.json) |
| <img src='https://flagcdn.com/gr.svg' width=20> &nbsp; Greek - Ελληνικά | `el` | 100% | [lang_el.json](src/Languages/lang_el.json) |
| <img src='https://flagcdn.com/ee.svg' width=20> &nbsp; Estonian - Eesti | `et` | 100% | [lang_et.json](src/Languages/lang_et.json) |
| <img src='https://flagcdn.com/gb.svg' width=20> &nbsp; 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> &nbsp; Esperanto - Esperanto | `eo` | 99% | [lang_eo.json](src/Languages/lang_eo.json) |
| <img src='https://flagcdn.com/es.svg' width=20> &nbsp; Spanish - Castellano | `es` | 100% | [lang_es.json](src/Languages/lang_es.json) |
| <img src='https://flagcdn.com/mx.svg' width=20> &nbsp; Spanish (Mexico) | `es-MX` | 100% | [lang_es-MX.json](src/Languages/lang_es-MX.json) |
| <img src='https://flagcdn.com/ir.svg' width=20> &nbsp; Persian - فارسی‎ | `fa` | 100% | [lang_fa.json](src/Languages/lang_fa.json) |
| <img src='https://flagcdn.com/fi.svg' width=20> &nbsp; Finnish - Suomi | `fi` | 100% | [lang_fi.json](src/Languages/lang_fi.json) |
| <img src='https://flagcdn.com/ph.svg' width=20> &nbsp; Filipino - Filipino | `fil` | 99% | [lang_fil.json](src/Languages/lang_fil.json) |
| <img src='https://flagcdn.com/fr.svg' width=20> &nbsp; French - Français | `fr` | 100% | [lang_fr.json](src/Languages/lang_fr.json) |
| <img src='https://flagcdn.com/es.svg' width=20> &nbsp; Galician - Galego | `gl` | 99% | [lang_gl.json](src/Languages/lang_gl.json) |
| <img src='https://flagcdn.com/in.svg' width=20> &nbsp; Gujarati - ગુજરાતી | `gu` | 100% | [lang_gu.json](src/Languages/lang_gu.json) |
| <img src='https://flagcdn.com/in.svg' width=20> &nbsp; Hindi - हिंदी | `hi` | 100% | [lang_hi.json](src/Languages/lang_hi.json) |
| <img src='https://flagcdn.com/hr.svg' width=20> &nbsp; Croatian - Hrvatski | `hr` | 100% | [lang_hr.json](src/Languages/lang_hr.json) |
| <img src='https://flagcdn.com/il.svg' width=20> &nbsp; Hebrew - עִבְרִית‎ | `he` | 100% | [lang_he.json](src/Languages/lang_he.json) |
| <img src='https://flagcdn.com/hu.svg' width=20> &nbsp; Hungarian - Magyar | `hu` | 100% | [lang_hu.json](src/Languages/lang_hu.json) |
| <img src='https://flagcdn.com/it.svg' width=20> &nbsp; Italian - Italiano | `it` | 100% | [lang_it.json](src/Languages/lang_it.json) |
| <img src='https://flagcdn.com/id.svg' width=20> &nbsp; Indonesian - Bahasa Indonesia | `id` | 100% | [lang_id.json](src/Languages/lang_id.json) |
| <img src='https://flagcdn.com/jp.svg' width=20> &nbsp; Japanese - 日本語 | `ja` | 100% | [lang_ja.json](src/Languages/lang_ja.json) |
| <img src='https://flagcdn.com/ge.svg' width=20> &nbsp; Georgian - ქართული | `ka` | 100% | [lang_ka.json](src/Languages/lang_ka.json) |
| <img src='https://flagcdn.com/in.svg' width=20> &nbsp; Kannada - ಕನ್ನಡ | `kn` | 100% | [lang_kn.json](src/Languages/lang_kn.json) |
| <img src='https://flagcdn.com/kr.svg' width=20> &nbsp; Korean - 한국어 | `ko` | 100% | [lang_ko.json](src/Languages/lang_ko.json) |
| <img src='https://flagcdn.com/iq.svg' width=20> &nbsp; Kurdish - کوردی | `ku` | 100% | [lang_ku.json](src/Languages/lang_ku.json) |
| <img src='https://flagcdn.com/lt.svg' width=20> &nbsp; Lithuanian - Lietuvių | `lt` | 100% | [lang_lt.json](src/Languages/lang_lt.json) |
| <img src='https://flagcdn.com/mk.svg' width=20> &nbsp; Macedonian - Македонски | `mk` | 100% | [lang_mk.json](src/Languages/lang_mk.json) |
| <img src='https://flagcdn.com/in.svg' width=20> &nbsp; Marathi - मराठी | `mr` | 100% | [lang_mr.json](src/Languages/lang_mr.json) |
| <img src='https://flagcdn.com/no.svg' width=20> &nbsp; Norwegian (bokmål) | `nb` | 100% | [lang_nb.json](src/Languages/lang_nb.json) |
| <img src='https://flagcdn.com/no.svg' width=20> &nbsp; Norwegian (nynorsk) | `nn` | 100% | [lang_nn.json](src/Languages/lang_nn.json) |
| <img src='https://flagcdn.com/nl.svg' width=20> &nbsp; Dutch - Nederlands | `nl` | 100% | [lang_nl.json](src/Languages/lang_nl.json) |
| <img src='https://flagcdn.com/pl.svg' width=20> &nbsp; Polish - Polski | `pl` | 100% | [lang_pl.json](src/Languages/lang_pl.json) |
| <img src='https://flagcdn.com/br.svg' width=20> &nbsp; Portuguese (Brazil) | `pt_BR` | 100% | [lang_pt_BR.json](src/Languages/lang_pt_BR.json) |
| <img src='https://flagcdn.com/pt.svg' width=20> &nbsp; Portuguese (Portugal) | `pt_PT` | 100% | [lang_pt_PT.json](src/Languages/lang_pt_PT.json) |
| <img src='https://flagcdn.com/ro.svg' width=20> &nbsp; Romanian - Română | `ro` | 100% | [lang_ro.json](src/Languages/lang_ro.json) |
| <img src='https://flagcdn.com/ru.svg' width=20> &nbsp; Russian - Русский | `ru` | 100% | [lang_ru.json](src/Languages/lang_ru.json) |
| <img src='https://flagcdn.com/in.svg' width=20> &nbsp; Sanskrit - संस्कृत भाषा | `sa` | 100% | [lang_sa.json](src/Languages/lang_sa.json) |
| <img src='https://flagcdn.com/sk.svg' width=20> &nbsp; Slovak - Slovenčina | `sk` | 100% | [lang_sk.json](src/Languages/lang_sk.json) |
| <img src='https://flagcdn.com/rs.svg' width=20> &nbsp; Serbian - Srpski | `sr` | 100% | [lang_sr.json](src/Languages/lang_sr.json) |
| <img src='https://flagcdn.com/al.svg' width=20> &nbsp; Albanian - Shqip | `sq` | 100% | [lang_sq.json](src/Languages/lang_sq.json) |
| <img src='https://flagcdn.com/lk.svg' width=20> &nbsp; Sinhala - සිංහල | `si` | 100% | [lang_si.json](src/Languages/lang_si.json) |
| <img src='https://flagcdn.com/si.svg' width=20> &nbsp; Slovene - Slovenščina | `sl` | 100% | [lang_sl.json](src/Languages/lang_sl.json) |
| <img src='https://flagcdn.com/se.svg' width=20> &nbsp; Swedish - Svenska | `sv` | 100% | [lang_sv.json](src/Languages/lang_sv.json) |
| <img src='https://flagcdn.com/in.svg' width=20> &nbsp; Tamil - தமிழ் | `ta` | 100% | [lang_ta.json](src/Languages/lang_ta.json) |
| <img src='https://flagcdn.com/tl.svg' width=20> &nbsp; Tagalog - Tagalog | `tl` | 100% | [lang_tl.json](src/Languages/lang_tl.json) |
| <img src='https://flagcdn.com/th.svg' width=20> &nbsp; Thai - ภาษาไทย | `th` | 100% | [lang_th.json](src/Languages/lang_th.json) |
| <img src='https://flagcdn.com/tr.svg' width=20> &nbsp; Turkish - Türkçe | `tr` | 100% | [lang_tr.json](src/Languages/lang_tr.json) |
| <img src='https://flagcdn.com/uk.svg' width=20> &nbsp; Ukrainian - Українська | `uk` | 100% | [lang_uk.json](src/Languages/lang_uk.json) |
| <img src='https://flagcdn.com/pk.svg' width=20> &nbsp; Urdu - اردو | `ur` | 100% | [lang_ur.json](src/Languages/lang_ur.json) |
| <img src='https://flagcdn.com/vn.svg' width=20> &nbsp; Vietnamese - Tiếng Việt | `vi` | 100% | [lang_vi.json](src/Languages/lang_vi.json) |
| <img src='https://flagcdn.com/cn.svg' width=20> &nbsp; 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> &nbsp; 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> &nbsp; Afrikaans - Afrikaans | `af` | Hendrik Bezuidenhout |
| <img src='https://flagcdn.com/sa.svg' width=20> &nbsp; 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> &nbsp; Belarusian - беларуская | `be` | [bthos](https://github.com/bthos) |
| <img src='https://flagcdn.com/bg.svg' width=20> &nbsp; Bulgarian - български | `bg` | Nikolay Naydenov, Vasil Kolev |
| <img src='https://flagcdn.com/bd.svg' width=20> &nbsp; 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> &nbsp; Catalan - Català | `ca` | [marticliment](https://github.com/marticliment) |
| <img src='https://flagcdn.com/cz.svg' width=20> &nbsp; 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> &nbsp; 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> &nbsp; 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> &nbsp; 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> &nbsp; Estonian - Eesti | `et` | [artjom3729](https://github.com/artjom3729) |
| <img src='https://flagcdn.com/gb.svg' width=20> &nbsp; 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> &nbsp; Esperanto - Esperanto | `eo` | |
| <img src='https://flagcdn.com/es.svg' width=20> &nbsp; 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> &nbsp; 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> &nbsp; Persian - فارسی‎ | `fa` | [ehinium](https://github.com/ehinium), [MobinMardi](https://github.com/MobinMardi) |
| <img src='https://flagcdn.com/fi.svg' width=20> &nbsp; Finnish - Suomi | `fi` | [simakuutio](https://github.com/simakuutio) |
| <img src='https://flagcdn.com/ph.svg' width=20> &nbsp; Filipino - Filipino | `fil` | [infyProductions](https://github.com/infyProductions) |
| <img src='https://flagcdn.com/fr.svg' width=20> &nbsp; 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> &nbsp; Galician - Galego | `gl` | |
| <img src='https://flagcdn.com/in.svg' width=20> &nbsp; Gujarati - ગુજરાતી | `gu` | |
| <img src='https://flagcdn.com/in.svg' width=20> &nbsp; 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> &nbsp; Croatian - Hrvatski | `hr` | [AndrejFeher](https://github.com/AndrejFeher), Ivan Nuić, Stjepan Treger |
| <img src='https://flagcdn.com/il.svg' width=20> &nbsp; Hebrew - עִבְרִית‎ | `he` | [maximunited](https://github.com/maximunited), Oryan Hassidim |
| <img src='https://flagcdn.com/hu.svg' width=20> &nbsp; Hungarian - Magyar | `hu` | [gidano](https://github.com/gidano) |
| <img src='https://flagcdn.com/it.svg' width=20> &nbsp; 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> &nbsp; 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> &nbsp; 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> &nbsp; Georgian - ქართული | `ka` | [marticliment](https://github.com/marticliment), [ppvnf](https://github.com/ppvnf) |
| <img src='https://flagcdn.com/in.svg' width=20> &nbsp; Kannada - ಕನ್ನಡ | `kn` | [skanda890](https://github.com/skanda890) |
| <img src='https://flagcdn.com/kr.svg' width=20> &nbsp; Korean - 한국어 | `ko` | [VenusGirl](https://github.com/VenusGirl) |
| <img src='https://flagcdn.com/iq.svg' width=20> &nbsp; Kurdish - کوردی | `ku` | |
| <img src='https://flagcdn.com/lt.svg' width=20> &nbsp; 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> &nbsp; Macedonian - Македонски | `mk` | LordDeatHunter |
| <img src='https://flagcdn.com/in.svg' width=20> &nbsp; Marathi - मराठी | `mr` | |
| <img src='https://flagcdn.com/no.svg' width=20> &nbsp; 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> &nbsp; Norwegian (nynorsk) | `nn` | [yrjarv](https://github.com/yrjarv) |
| <img src='https://flagcdn.com/nl.svg' width=20> &nbsp; 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> &nbsp; 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> &nbsp; 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> &nbsp; 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> &nbsp; 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> &nbsp; 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> &nbsp; Sanskrit - संस्कृत भाषा | `sa` | [skanda890](https://github.com/skanda890) |
| <img src='https://flagcdn.com/sk.svg' width=20> &nbsp; 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> &nbsp; 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> &nbsp; Albanian - Shqip | `sq` | [RDN000](https://github.com/RDN000) |
| <img src='https://flagcdn.com/lk.svg' width=20> &nbsp; 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> &nbsp; Slovene - Slovenščina | `sl` | [rumplin](https://github.com/rumplin) |
| <img src='https://flagcdn.com/se.svg' width=20> &nbsp; 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> &nbsp; Tamil - தமிழ் | `ta` | [nochilli](https://github.com/nochilli) |
| <img src='https://flagcdn.com/tl.svg' width=20> &nbsp; Tagalog - Tagalog | `tl` | lasersPew, [znarfm](https://github.com/znarfm) |
| <img src='https://flagcdn.com/th.svg' width=20> &nbsp; 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> &nbsp; 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> &nbsp; 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> &nbsp; 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> &nbsp; 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> &nbsp; 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> &nbsp; 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
```
+352
View File
@@ -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"
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+10
View File
@@ -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
+65
View File
@@ -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.
+26
View File
@@ -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
+262
View File
@@ -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 UniGetUIs 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. |
+397
View File
@@ -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.
+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "latestFeature"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.6 KiB

File diff suppressed because one or more lines are too long

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

Some files were not shown because too many files have changed in this diff Show More