Skip to content

Commit 5a04cb5

Browse files
Add package operation automation parity
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2e6075e commit 5a04cb5

File tree

6 files changed

+253
-4
lines changed

6 files changed

+253
-4
lines changed

cli-arguments.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@
6767
| `--automation search-packages --manager name --query text [--max-results n]` | Searches packages through the automation service and returns structured JSON | 2026.1+ |
6868
| `--automation package-details --manager name --package-id id` | Fetches the package-details payload currently exposed through the automation layer | 2026.1+ |
6969
| `--automation package-versions --manager name --package-id id` | Lists installable versions for a package when the manager supports custom versions | 2026.1+ |
70-
| `--automation install-package --manager name --package-id id [--version v] [--scope scope] [--pre-release]` | Installs a package through the automation service and waits for completion | 2026.1+ |
70+
| `--automation install-package --manager name --package-id id [--version v] [--scope scope] [--pre-release] [--elevated true\|false] [--interactive true\|false] [--skip-hash true\|false] [--architecture value] [--location path]` | Installs a package through the automation service and waits for completion, honoring the same core install options exposed by the UI | 2026.1+ |
71+
| `--automation download-package --manager name --package-id id --output path` | Downloads a package installer or artifact to the specified file or directory and returns the resolved saved path | 2026.1+ |
72+
| `--automation reinstall-package --manager name --package-id id [--version v] [--scope scope] [--pre-release] [--elevated true\|false] [--interactive true\|false] [--skip-hash true\|false] [--architecture value] [--location path]` | Re-runs package installation for an installed package using the requested install options | 2026.1+ |
7173
| `--automation open-window` | Asks the running UniGetUI instance to show the main window | 2026.1+ |
7274
| `--automation open-updates` | Asks the running UniGetUI instance to show the Updates page | 2026.1+ |
7375
| `--automation show-package --package-id id --package-source source` | Opens the package details flow for the specified package | 2026.1+ |
@@ -76,8 +78,9 @@
7678
| `--automation unignore-package --manager name --package-id id [--version v]` | Removes an ignored-update rule for a package and refreshes the updates view | 2026.1+ |
7779
| `--automation update-all` | Queues updates for all packages currently shown as upgradable | 2026.1+ |
7880
| `--automation update-manager --manager name` | Queues updates for all packages handled by the specified manager | 2026.1+ |
79-
| `--automation update-package --manager name --package-id id [--version v]` | Updates a specific package through the automation service and waits for completion | 2026.1+ |
80-
| `--automation uninstall-package --manager name --package-id id [--scope scope]` | Uninstalls a package through the automation service and waits for completion | 2026.1+ |
81+
| `--automation update-package --manager name --package-id id [--version v] [--scope scope] [--pre-release] [--elevated true\|false] [--interactive true\|false] [--skip-hash true\|false] [--architecture value] [--location path]` | Updates a specific package through the automation service and waits for completion | 2026.1+ |
82+
| `--automation uninstall-package --manager name --package-id id [--scope scope] [--remove-data true\|false] [--elevated true\|false] [--interactive true\|false]` | Uninstalls a package through the automation service and waits for completion | 2026.1+ |
83+
| `--automation uninstall-then-reinstall-package --manager name --package-id id [--version v] [--scope scope] [--pre-release] [--remove-data true\|false] [--elevated true\|false] [--interactive true\|false] [--skip-hash true\|false] [--architecture value] [--location path]` | Uninstalls an installed package and then immediately reinstalls it through the shared operation pipeline | 2026.1+ |
8184
| `--background-api-transport {tcp\|named-pipe}` | Selects which local HTTP transport UniGetUI uses for the background API when the app starts | 2026.1+ |
8285
| `--background-api-port port` | Overrides the localhost TCP port used by the background API when `--background-api-transport tcp` is active | 2026.1+ |
8386
| `--background-api-pipe-name name` | Overrides the Windows named pipe name used by the background API when `--background-api-transport named-pipe` is active | 2026.1+ |

src/UniGetUI.Interface.BackgroundApi/AutomationCliCommandRunner.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,14 @@ await client.RemoveIgnoredUpdateAsync(BuildPackageActionRequest(args))
409409
output,
410410
await client.InstallPackageAsync(BuildPackageActionRequest(args))
411411
),
412+
"download-package" => await WriteJsonAsync(
413+
output,
414+
await client.DownloadPackageAsync(BuildPackageActionRequest(args))
415+
),
416+
"reinstall-package" => await WriteJsonAsync(
417+
output,
418+
await client.ReinstallPackageAsync(BuildPackageActionRequest(args))
419+
),
412420
"update-package" => await WriteJsonAsync(
413421
output,
414422
await client.UpdatePackageAsync(BuildPackageActionRequest(args))
@@ -417,6 +425,10 @@ await client.UpdatePackageAsync(BuildPackageActionRequest(args))
417425
output,
418426
await client.UninstallPackageAsync(BuildPackageActionRequest(args))
419427
),
428+
"uninstall-then-reinstall-package" => await WriteJsonAsync(
429+
output,
430+
await client.UninstallThenReinstallPackageAsync(BuildPackageActionRequest(args))
431+
),
420432
"open-window" => await WriteJsonAsync(output, await client.OpenWindowAsync()),
421433
"open-updates" => await WriteJsonAsync(output, await client.OpenUpdatesAsync()),
422434
"show-package" => await WriteJsonAsync(
@@ -493,6 +505,13 @@ private static AutomationPackageActionRequest BuildPackageActionRequest(IReadOnl
493505
Version = GetOptionalArgument(args, "--version"),
494506
Scope = GetOptionalArgument(args, "--scope"),
495507
PreRelease = args.Contains("--pre-release") ? true : null,
508+
Elevated = GetOptionalBoolArgument(args, "--elevated"),
509+
Interactive = GetOptionalBoolArgument(args, "--interactive"),
510+
SkipHash = GetOptionalBoolArgument(args, "--skip-hash"),
511+
RemoveData = GetOptionalBoolArgument(args, "--remove-data"),
512+
Architecture = GetOptionalArgument(args, "--architecture"),
513+
InstallLocation = GetOptionalArgument(args, "--location"),
514+
OutputPath = GetOptionalArgument(args, "--output"),
496515
};
497516
}
498517

src/UniGetUI.Interface.BackgroundApi/AutomationPackageApi.cs

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ public sealed class AutomationPackageActionRequest
3030
public string? Version { get; set; }
3131
public string? Scope { get; set; }
3232
public bool? PreRelease { get; set; }
33+
public bool? Elevated { get; set; }
34+
public bool? Interactive { get; set; }
35+
public bool? SkipHash { get; set; }
36+
public bool? RemoveData { get; set; }
37+
public string? Architecture { get; set; }
38+
public string? InstallLocation { get; set; }
39+
public string? OutputPath { get; set; }
3340
}
3441

3542
public sealed class AutomationPackageOperationResult
@@ -39,6 +46,7 @@ public sealed class AutomationPackageOperationResult
3946
public string OperationStatus { get; set; } = "";
4047
public string? Message { get; set; }
4148
public AutomationPackageInfo? Package { get; set; }
49+
public string? OutputPath { get; set; }
4250
public IReadOnlyList<string> Output { get; set; } = [];
4351
}
4452

@@ -187,6 +195,74 @@ AutomationPackageActionRequest request
187195
);
188196
}
189197

198+
public static async Task<AutomationPackageOperationResult> DownloadPackageAsync(
199+
AutomationPackageActionRequest request
200+
)
201+
{
202+
ArgumentNullException.ThrowIfNull(request);
203+
204+
if (string.IsNullOrWhiteSpace(request.OutputPath))
205+
{
206+
throw new InvalidOperationException(
207+
"The outputPath parameter is required when downloading a package."
208+
);
209+
}
210+
211+
var package = FindAnyPackage(request);
212+
if (!package.Manager.Capabilities.CanDownloadInstaller)
213+
{
214+
throw new InvalidOperationException(
215+
$"The manager \"{package.Manager.Name}\" does not support installer downloads."
216+
);
217+
}
218+
219+
using var operation = new DownloadOperation(package, request.OutputPath);
220+
await operation.MainThread();
221+
222+
return CreateOperationResult(
223+
"download-package",
224+
package,
225+
operation,
226+
operation.DownloadLocation
227+
);
228+
}
229+
230+
public static Task<AutomationPackageOperationResult> ReinstallPackageAsync(
231+
AutomationPackageActionRequest request
232+
)
233+
{
234+
ArgumentNullException.ThrowIfNull(request);
235+
236+
var package = FindInstalledPackage(request);
237+
return ExecuteOperationAsync(
238+
"reinstall-package",
239+
package,
240+
request,
241+
(pkg, options) => new InstallPackageOperation(pkg, options)
242+
);
243+
}
244+
245+
public static async Task<AutomationPackageOperationResult> UninstallThenReinstallPackageAsync(
246+
AutomationPackageActionRequest request
247+
)
248+
{
249+
ArgumentNullException.ThrowIfNull(request);
250+
251+
var package = FindInstalledPackage(request);
252+
var options = await InstallOptionsFactory.LoadApplicableAsync(package);
253+
ApplyRequestOptions(options, request);
254+
255+
using var uninstallOperation = new UninstallPackageOperation(package, options);
256+
using var installOperation = new InstallPackageOperation(
257+
package,
258+
options,
259+
req: uninstallOperation
260+
);
261+
await installOperation.MainThread();
262+
263+
return CreateOperationResult("uninstall-then-reinstall-package", package, installOperation);
264+
}
265+
190266
public static async Task<AutomationPackageDetailsInfo> GetPackageDetailsAsync(
191267
AutomationPackageActionRequest request
192268
)
@@ -350,6 +426,36 @@ AutomationPackageActionRequest request
350426
{
351427
options.PreRelease = request.PreRelease.Value;
352428
}
429+
430+
if (request.Elevated.HasValue)
431+
{
432+
options.RunAsAdministrator = request.Elevated.Value;
433+
}
434+
435+
if (request.Interactive.HasValue)
436+
{
437+
options.InteractiveInstallation = request.Interactive.Value;
438+
}
439+
440+
if (request.SkipHash.HasValue)
441+
{
442+
options.SkipHashCheck = request.SkipHash.Value;
443+
}
444+
445+
if (request.RemoveData.HasValue)
446+
{
447+
options.RemoveDataOnUninstall = request.RemoveData.Value;
448+
}
449+
450+
if (!string.IsNullOrWhiteSpace(request.Architecture))
451+
{
452+
options.Architecture = request.Architecture;
453+
}
454+
455+
if (!string.IsNullOrWhiteSpace(request.InstallLocation))
456+
{
457+
options.CustomInstallLocation = request.InstallLocation;
458+
}
353459
}
354460

355461
internal static void ApplyRequestedOptions(
@@ -385,7 +491,8 @@ internal static AutomationPackageInfo CreateAutomationPackageInfo(IPackage packa
385491
internal static AutomationPackageOperationResult CreateOperationResult(
386492
string command,
387493
IPackage package,
388-
AbstractOperation operation
494+
AbstractOperation operation,
495+
string? outputPath = null
389496
)
390497
{
391498
return new AutomationPackageOperationResult
@@ -400,6 +507,7 @@ AbstractOperation operation
400507
_ => operation.GetOutput().LastOrDefault().Item1,
401508
},
402509
Package = ToAutomationPackageInfo(package),
510+
OutputPath = outputPath,
403511
Output = operation.GetOutput().Select(line => line.Item1).ToArray(),
404512
};
405513
}

src/UniGetUI.Interface.BackgroundApi/BackgroundApi.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,15 @@ public async Task Start()
133133
endpoints.MapGet("/v3/packages/ignored", V3_ListIgnoredUpdates);
134134
endpoints.MapPost("/v3/packages/ignore", V3_IgnorePackage);
135135
endpoints.MapPost("/v3/packages/unignore", V3_UnignorePackage);
136+
endpoints.MapPost("/v3/packages/download", V3_DownloadPackage);
136137
endpoints.MapPost("/v3/packages/install", V3_InstallPackage);
138+
endpoints.MapPost("/v3/packages/reinstall", V3_ReinstallPackage);
137139
endpoints.MapPost("/v3/packages/update", V3_UpdatePackage);
138140
endpoints.MapPost("/v3/packages/uninstall", V3_UninstallPackage);
141+
endpoints.MapPost(
142+
"/v3/packages/uninstall-then-reinstall",
143+
V3_UninstallThenReinstallPackage
144+
);
139145
// Share endpoints
140146
endpoints.MapGet("/v2/show-package", V2_ShowPackage);
141147
endpoints.MapGet("/is-running", API_IsRunning);
@@ -1308,6 +1314,22 @@ await HandlePackageActionAsync(
13081314
);
13091315
}
13101316

1317+
private async Task V3_DownloadPackage(HttpContext context)
1318+
{
1319+
await HandlePackageActionAsync(
1320+
context,
1321+
AutomationPackageApi.DownloadPackageAsync
1322+
);
1323+
}
1324+
1325+
private async Task V3_ReinstallPackage(HttpContext context)
1326+
{
1327+
await HandlePackageActionAsync(
1328+
context,
1329+
AutomationPackageApi.ReinstallPackageAsync
1330+
);
1331+
}
1332+
13111333
private async Task V3_UpdatePackage(HttpContext context)
13121334
{
13131335
await HandlePackageActionAsync(
@@ -1324,6 +1346,14 @@ await HandlePackageActionAsync(
13241346
);
13251347
}
13261348

1349+
private async Task V3_UninstallThenReinstallPackage(HttpContext context)
1350+
{
1351+
await HandlePackageActionAsync(
1352+
context,
1353+
AutomationPackageApi.UninstallThenReinstallPackageAsync
1354+
);
1355+
}
1356+
13271357
private static async Task HandlePackageActionAsync(
13281358
HttpContext context,
13291359
Func<AutomationPackageActionRequest, Task<AutomationPackageOperationResult>> action
@@ -1550,6 +1580,21 @@ private static AutomationPackageActionRequest BuildPackageActionRequest(HttpRequ
15501580
PreRelease = bool.TryParse(request.Query["preRelease"], out bool preRelease)
15511581
? preRelease
15521582
: null,
1583+
Elevated = bool.TryParse(request.Query["elevated"], out bool elevated)
1584+
? elevated
1585+
: null,
1586+
Interactive = bool.TryParse(request.Query["interactive"], out bool interactive)
1587+
? interactive
1588+
: null,
1589+
SkipHash = bool.TryParse(request.Query["skipHash"], out bool skipHash)
1590+
? skipHash
1591+
: null,
1592+
RemoveData = bool.TryParse(request.Query["removeData"], out bool removeData)
1593+
? removeData
1594+
: null,
1595+
Architecture = request.Query["architecture"],
1596+
InstallLocation = request.Query["location"],
1597+
OutputPath = request.Query["outputPath"],
15531598
};
15541599
}
15551600

src/UniGetUI.Interface.BackgroundApi/BackgroundApiClient.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,20 @@ AutomationPackageActionRequest request
748748
return await SendPackageOperationAsync("/v3/packages/install", request);
749749
}
750750

751+
public async Task<AutomationPackageOperationResult> DownloadPackageAsync(
752+
AutomationPackageActionRequest request
753+
)
754+
{
755+
return await SendPackageOperationAsync("/v3/packages/download", request);
756+
}
757+
758+
public async Task<AutomationPackageOperationResult> ReinstallPackageAsync(
759+
AutomationPackageActionRequest request
760+
)
761+
{
762+
return await SendPackageOperationAsync("/v3/packages/reinstall", request);
763+
}
764+
751765
public async Task<AutomationPackageOperationResult> UpdatePackageAsync(
752766
AutomationPackageActionRequest request
753767
)
@@ -762,6 +776,16 @@ AutomationPackageActionRequest request
762776
return await SendPackageOperationAsync("/v3/packages/uninstall", request);
763777
}
764778

779+
public async Task<AutomationPackageOperationResult> UninstallThenReinstallPackageAsync(
780+
AutomationPackageActionRequest request
781+
)
782+
{
783+
return await SendPackageOperationAsync(
784+
"/v3/packages/uninstall-then-reinstall",
785+
request
786+
);
787+
}
788+
765789
public static IReadOnlyList<BackgroundApiUpdateEntry> ParseUpdatesPayload(string payload)
766790
{
767791
if (string.IsNullOrWhiteSpace(payload))
@@ -1026,6 +1050,41 @@ AutomationPackageActionRequest request
10261050
parameters["preRelease"] = request.PreRelease.Value ? "true" : "false";
10271051
}
10281052

1053+
if (request.Elevated.HasValue)
1054+
{
1055+
parameters["elevated"] = request.Elevated.Value ? "true" : "false";
1056+
}
1057+
1058+
if (request.Interactive.HasValue)
1059+
{
1060+
parameters["interactive"] = request.Interactive.Value ? "true" : "false";
1061+
}
1062+
1063+
if (request.SkipHash.HasValue)
1064+
{
1065+
parameters["skipHash"] = request.SkipHash.Value ? "true" : "false";
1066+
}
1067+
1068+
if (request.RemoveData.HasValue)
1069+
{
1070+
parameters["removeData"] = request.RemoveData.Value ? "true" : "false";
1071+
}
1072+
1073+
if (!string.IsNullOrWhiteSpace(request.Architecture))
1074+
{
1075+
parameters["architecture"] = request.Architecture;
1076+
}
1077+
1078+
if (!string.IsNullOrWhiteSpace(request.InstallLocation))
1079+
{
1080+
parameters["location"] = request.InstallLocation;
1081+
}
1082+
1083+
if (!string.IsNullOrWhiteSpace(request.OutputPath))
1084+
{
1085+
parameters["outputPath"] = request.OutputPath;
1086+
}
1087+
10291088
return parameters;
10301089
}
10311090

testing/automation/run-cli-e2e.ps1

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ $cliProject = (Resolve-Path $cliProject).Path
2525

2626
$daemonRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("unigetui-headless-" + [Guid]::NewGuid().ToString('N'))
2727
New-Item -ItemType Directory -Path $daemonRoot | Out-Null
28+
$downloadRoot = Join-Path $daemonRoot 'downloads'
29+
New-Item -ItemType Directory -Path $downloadRoot | Out-Null
2830

2931
$env:HOME = $daemonRoot
3032
$env:USERPROFILE = $daemonRoot
@@ -336,6 +338,19 @@ try {
336338
throw "package-versions did not report version 2.1.4 for dotnetsay"
337339
}
338340

341+
$download = Invoke-CliJson -Arguments @(
342+
'download-package',
343+
'--manager', '.NET Tool',
344+
'--package-id', 'dotnetsay',
345+
'--output', $downloadRoot
346+
)
347+
if ($download.status -ne 'success' -or [string]::IsNullOrWhiteSpace($download.outputPath)) {
348+
throw "download-package failed: $($download | ConvertTo-Json -Depth 8)"
349+
}
350+
if (-not (Test-Path $download.outputPath)) {
351+
throw "download-package did not create the downloaded file at $($download.outputPath)"
352+
}
353+
339354
Write-Stage 'Bundle roundtrip'
340355
Write-Host ' - reset bundle'
341356
$resetBundle = Invoke-CliJson -Arguments @('reset-bundle')

0 commit comments

Comments
 (0)