Skip to content

Commit 2b5d9bd

Browse files
Add desktop shortcut automation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 857247c commit 2b5d9bd

File tree

7 files changed

+460
-1
lines changed

7 files changed

+460
-1
lines changed

cli-arguments.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
| `--automation set-setting --key key (--enabled true|false \| --value text)` | Sets a boolean or string setting through the automation service | 2026.1+ |
3131
| `--automation clear-setting --key key` | Clears a string-backed setting through the automation service | 2026.1+ |
3232
| `--automation reset-settings` | Resets non-secure settings while preserving the active automation session token | 2026.1+ |
33+
| `--automation list-desktop-shortcuts` | Lists tracked desktop shortcuts, their current keep/delete/unknown verdicts, and whether each shortcut still exists on disk | 2026.1+ |
34+
| `--automation set-desktop-shortcut --path path --status {keep\|delete}` | Marks a tracked shortcut to be kept or deleted; `delete` also removes the shortcut from disk when present | 2026.1+ |
35+
| `--automation reset-desktop-shortcut --path path` | Clears the stored verdict for one tracked desktop shortcut | 2026.1+ |
36+
| `--automation reset-desktop-shortcuts` | Clears all stored desktop-shortcut verdicts | 2026.1+ |
3337
| `--automation get-app-log [--level n]` | Reads the UniGetUI application log as structured JSON, with optional severity filtering | 2026.1+ |
3438
| `--automation get-operation-history` | Reads the persisted operation history shown by the log/history UI surfaces | 2026.1+ |
3539
| `--automation get-manager-log [--manager name] [--verbose]` | Reads manager task logs, optionally for one manager and with verbose subprocess/stdin/stdout detail | 2026.1+ |
@@ -65,7 +69,7 @@
6569

6670
- `dotnet src\UniGetUI.Avalonia\bin\Release\net10.0\UniGetUI.Avalonia.dll --headless` starts the local automation daemon without opening any window or requiring a graphical desktop session.
6771
- `dotnet src\UniGetUI.Cli\bin\Release\net10.0\UniGetUI.Cli.dll <command>` is the cross-platform CLI wrapper for the automation service. It automatically prepends `--automation`, so `UniGetUI.Cli status` and `UniGetUI.Cli search-packages --manager ".NET Tool" --query dotnetsay` work directly.
68-
- Current agent-oriented command coverage includes status/version, manager/source inspection, settings inspection and mutation, app/history/manager log inspection, package search/details/version listing, ignored-update management, and package install/update/uninstall flows.
72+
- Current agent-oriented command coverage includes status/version, manager/source inspection, settings inspection and mutation, desktop-shortcut state management, app/history/manager log inspection, package search/details/version listing, ignored-update management, and package install/update/uninstall flows.
6973

7074
<br><br>
7175
# `unigetui://` deep link

src/UniGetUI.Interface.BackgroundApi/AutomationCliCommandRunner.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,32 @@ await client.RemoveSourceAsync(BuildSourceRequest(args))
113113
output,
114114
await client.ResetSettingsAsync()
115115
),
116+
"list-desktop-shortcuts" => await WriteJsonAsync(
117+
output,
118+
new
119+
{
120+
status = "success",
121+
shortcuts = await client.ListDesktopShortcutsAsync(),
122+
}
123+
),
124+
"set-desktop-shortcut" => await WriteJsonAsync(
125+
output,
126+
await client.SetDesktopShortcutAsync(BuildDesktopShortcutRequest(args, requireStatus: true))
127+
),
128+
"reset-desktop-shortcut" => await WriteJsonAsync(
129+
output,
130+
await client.ResetDesktopShortcutAsync(
131+
GetRequiredArgument(
132+
args,
133+
"--path",
134+
"The reset-desktop-shortcut automation command requires --path."
135+
)
136+
)
137+
),
138+
"reset-desktop-shortcuts" => await WriteJsonAsync(
139+
output,
140+
await client.ResetDesktopShortcutsAsync()
141+
),
116142
"get-app-log" => await WriteJsonAsync(
117143
output,
118144
new
@@ -325,6 +351,24 @@ private static AutomationSourceRequest BuildSourceRequest(IReadOnlyList<string>
325351
};
326352
}
327353

354+
private static AutomationDesktopShortcutRequest BuildDesktopShortcutRequest(
355+
IReadOnlyList<string> args,
356+
bool requireStatus
357+
)
358+
{
359+
return new AutomationDesktopShortcutRequest
360+
{
361+
Path = GetRequiredArgument(args, "--path", "This automation command requires --path."),
362+
Status = requireStatus
363+
? GetRequiredArgument(
364+
args,
365+
"--status",
366+
"This automation command requires --status."
367+
)
368+
: GetOptionalArgument(args, "--status"),
369+
};
370+
}
371+
328372
private static AutomationSettingValueRequest BuildSettingRequest(IReadOnlyList<string> args)
329373
{
330374
bool? enabled = null;
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using UniGetUI.PackageEngine.Classes.Packages.Classes;
2+
3+
namespace UniGetUI.Interface;
4+
5+
public sealed class AutomationDesktopShortcutInfo
6+
{
7+
public string Path { get; set; } = "";
8+
public string Name { get; set; } = "";
9+
public string Status { get; set; } = "";
10+
public bool ExistsOnDisk { get; set; }
11+
public bool IsTracked { get; set; }
12+
public bool IsPendingReview { get; set; }
13+
}
14+
15+
public sealed class AutomationDesktopShortcutRequest
16+
{
17+
public string Path { get; set; } = "";
18+
public string? Status { get; set; }
19+
}
20+
21+
public sealed class AutomationDesktopShortcutOperationResult
22+
{
23+
public string Status { get; set; } = "success";
24+
public string Command { get; set; } = "";
25+
public string? Message { get; set; }
26+
public AutomationDesktopShortcutInfo? Shortcut { get; set; }
27+
}
28+
29+
public static class AutomationDesktopShortcutsApi
30+
{
31+
public static IReadOnlyList<AutomationDesktopShortcutInfo> ListShortcuts()
32+
{
33+
var trackedShortcuts = DesktopShortcutsDatabase.GetDatabase();
34+
HashSet<string> allShortcuts =
35+
[
36+
.. DesktopShortcutsDatabase.GetAllShortcuts(),
37+
.. DesktopShortcutsDatabase.GetUnknownShortcuts(),
38+
];
39+
40+
return allShortcuts
41+
.OrderBy(path => System.IO.Path.GetFileName(path), StringComparer.OrdinalIgnoreCase)
42+
.ThenBy(path => path, StringComparer.OrdinalIgnoreCase)
43+
.Select(path => ToShortcutInfo(path, trackedShortcuts))
44+
.ToArray();
45+
}
46+
47+
public static AutomationDesktopShortcutOperationResult SetShortcut(
48+
AutomationDesktopShortcutRequest request
49+
)
50+
{
51+
string shortcutPath = NormalizeShortcutPath(request.Path);
52+
string shortcutStatus = request.Status?.Trim().ToLowerInvariant() ?? "";
53+
54+
DesktopShortcutsDatabase.Status status = shortcutStatus switch
55+
{
56+
"delete" => DesktopShortcutsDatabase.Status.Delete,
57+
"keep" => DesktopShortcutsDatabase.Status.Maintain,
58+
_ => throw new InvalidOperationException(
59+
"The status parameter must be either keep or delete."
60+
),
61+
};
62+
63+
DesktopShortcutsDatabase.AddToDatabase(shortcutPath, status);
64+
DesktopShortcutsDatabase.RemoveFromUnknownShortcuts(shortcutPath);
65+
66+
if (status is DesktopShortcutsDatabase.Status.Delete && File.Exists(shortcutPath))
67+
{
68+
DesktopShortcutsDatabase.DeleteFromDisk(shortcutPath);
69+
}
70+
71+
return new AutomationDesktopShortcutOperationResult
72+
{
73+
Command = "set-desktop-shortcut",
74+
Shortcut = ToShortcutInfo(shortcutPath),
75+
};
76+
}
77+
78+
public static AutomationDesktopShortcutOperationResult ResetShortcut(
79+
AutomationDesktopShortcutRequest request
80+
)
81+
{
82+
string shortcutPath = NormalizeShortcutPath(request.Path);
83+
DesktopShortcutsDatabase.AddToDatabase(shortcutPath, DesktopShortcutsDatabase.Status.Unknown);
84+
85+
return new AutomationDesktopShortcutOperationResult
86+
{
87+
Command = "reset-desktop-shortcut",
88+
Shortcut = ToShortcutInfo(shortcutPath),
89+
};
90+
}
91+
92+
public static BackgroundApiCommandResult ResetAllShortcuts()
93+
{
94+
DesktopShortcutsDatabase.ResetDatabase();
95+
return BackgroundApiCommandResult.Success("reset-desktop-shortcuts");
96+
}
97+
98+
private static AutomationDesktopShortcutInfo ToShortcutInfo(
99+
string shortcutPath,
100+
IReadOnlyDictionary<string, bool>? trackedShortcuts = null
101+
)
102+
{
103+
trackedShortcuts ??= DesktopShortcutsDatabase.GetDatabase();
104+
string fileName = System.IO.Path.GetFileName(shortcutPath);
105+
106+
return new AutomationDesktopShortcutInfo
107+
{
108+
Path = shortcutPath,
109+
Name = string.IsNullOrWhiteSpace(fileName)
110+
? shortcutPath
111+
: System.IO.Path.GetFileNameWithoutExtension(fileName),
112+
Status = DesktopShortcutsDatabase.GetStatus(shortcutPath) switch
113+
{
114+
DesktopShortcutsDatabase.Status.Delete => "delete",
115+
DesktopShortcutsDatabase.Status.Maintain => "keep",
116+
_ => "unknown",
117+
},
118+
ExistsOnDisk = File.Exists(shortcutPath),
119+
IsTracked = trackedShortcuts.ContainsKey(shortcutPath),
120+
IsPendingReview = DesktopShortcutsDatabase.GetUnknownShortcuts().Contains(shortcutPath),
121+
};
122+
}
123+
124+
private static string NormalizeShortcutPath(string shortcutPath)
125+
{
126+
string normalizedPath = shortcutPath.Trim().Trim('"').Trim('\'');
127+
if (string.IsNullOrWhiteSpace(normalizedPath))
128+
{
129+
throw new InvalidOperationException("The path parameter is required.");
130+
}
131+
132+
return normalizedPath;
133+
}
134+
}

src/UniGetUI.Interface.BackgroundApi/BackgroundApi.cs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ public async Task Start()
7777
endpoints.MapPost("/v3/settings/set", V3_SetSetting);
7878
endpoints.MapPost("/v3/settings/clear", V3_ClearSetting);
7979
endpoints.MapPost("/v3/settings/reset", V3_ResetSettings);
80+
endpoints.MapGet("/v3/desktop-shortcuts", V3_ListDesktopShortcuts);
81+
endpoints.MapPost("/v3/desktop-shortcuts/set", V3_SetDesktopShortcut);
82+
endpoints.MapPost("/v3/desktop-shortcuts/reset", V3_ResetDesktopShortcut);
83+
endpoints.MapPost("/v3/desktop-shortcuts/reset-all", V3_ResetDesktopShortcuts);
8084
endpoints.MapGet("/v3/logs/app", V3_GetAppLog);
8185
endpoints.MapGet("/v3/logs/history", V3_GetOperationHistory);
8286
endpoints.MapGet("/v3/logs/manager", V3_GetManagerLog);
@@ -392,6 +396,107 @@ await context.Response.WriteAsJsonAsync(
392396
);
393397
}
394398

399+
private async Task V3_ListDesktopShortcuts(HttpContext context)
400+
{
401+
if (!AuthenticateToken(context.Request.Query["token"]))
402+
{
403+
context.Response.StatusCode = 401;
404+
return;
405+
}
406+
407+
await context.Response.WriteAsJsonAsync(
408+
AutomationDesktopShortcutsApi.ListShortcuts(),
409+
new JsonSerializerOptions(SerializationHelpers.DefaultOptions)
410+
{
411+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
412+
WriteIndented = true,
413+
}
414+
);
415+
}
416+
417+
private async Task V3_SetDesktopShortcut(HttpContext context)
418+
{
419+
if (!AuthenticateToken(context.Request.Query["token"]))
420+
{
421+
context.Response.StatusCode = 401;
422+
return;
423+
}
424+
425+
try
426+
{
427+
await context.Response.WriteAsJsonAsync(
428+
AutomationDesktopShortcutsApi.SetShortcut(
429+
new AutomationDesktopShortcutRequest
430+
{
431+
Path = context.Request.Query["path"],
432+
Status = context.Request.Query.TryGetValue("status", out var status)
433+
? status.ToString()
434+
: null,
435+
}
436+
),
437+
new JsonSerializerOptions(SerializationHelpers.DefaultOptions)
438+
{
439+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
440+
WriteIndented = true,
441+
}
442+
);
443+
}
444+
catch (InvalidOperationException ex)
445+
{
446+
context.Response.StatusCode = 400;
447+
await context.Response.WriteAsync(ex.Message);
448+
}
449+
}
450+
451+
private async Task V3_ResetDesktopShortcut(HttpContext context)
452+
{
453+
if (!AuthenticateToken(context.Request.Query["token"]))
454+
{
455+
context.Response.StatusCode = 401;
456+
return;
457+
}
458+
459+
try
460+
{
461+
await context.Response.WriteAsJsonAsync(
462+
AutomationDesktopShortcutsApi.ResetShortcut(
463+
new AutomationDesktopShortcutRequest
464+
{
465+
Path = context.Request.Query["path"],
466+
}
467+
),
468+
new JsonSerializerOptions(SerializationHelpers.DefaultOptions)
469+
{
470+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
471+
WriteIndented = true,
472+
}
473+
);
474+
}
475+
catch (InvalidOperationException ex)
476+
{
477+
context.Response.StatusCode = 400;
478+
await context.Response.WriteAsync(ex.Message);
479+
}
480+
}
481+
482+
private async Task V3_ResetDesktopShortcuts(HttpContext context)
483+
{
484+
if (!AuthenticateToken(context.Request.Query["token"]))
485+
{
486+
context.Response.StatusCode = 401;
487+
return;
488+
}
489+
490+
await context.Response.WriteAsJsonAsync(
491+
AutomationDesktopShortcutsApi.ResetAllShortcuts(),
492+
new JsonSerializerOptions(SerializationHelpers.DefaultOptions)
493+
{
494+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
495+
WriteIndented = true,
496+
}
497+
);
498+
}
499+
395500
private async Task V3_GetAppLog(HttpContext context)
396501
{
397502
if (!AuthenticateToken(context.Request.Query["token"]))

0 commit comments

Comments
 (0)