Skip to content

Commit a3fb243

Browse files
committed
feat(plugins): implement automatic plugin downloading and document it
1 parent 1856130 commit a3fb243

4 files changed

Lines changed: 108 additions & 0 deletions

File tree

docs/modules/plugins.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,12 @@ extensions:
178178
test-echo:
179179
message: 'Hello from Python Plugin!'
180180
```
181+
182+
---
183+
184+
## Plugin Auto-Discovery & Download
185+
186+
WinHome makes it seamless to use plugins without having to manually copy files:
187+
- **On-Demand Auto-Download**: If you define an extension or top-level plugin in your `config.yaml` that is not present in `%LOCALAPPDATA%\WinHome\plugins`, WinHome will automatically download the missing plugin from GitHub and extract it into your local directory at runtime.
188+
- **Auto-Installation of Target Apps**: By using the `-i` or `--auto-install-apps` CLI flag, WinHome will check if the target applications (e.g. VLC, chezmoi, or Git) for your active plugins are installed. If they are missing, it will automatically install them using their default package manager (such as `winget`, `scoop`, or `choco`) before applying your configuration.
189+

src/Engine.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ public async Task RunAsync(Configuration config, bool dryRun, string? profileNam
9595
{
9696
_logger.LogInfo($"--- WinHome v{config.Version} ---");
9797

98+
// Ensure all configured plugins are downloaded/available locally
99+
var configuredPluginNames = config.Apps.Select(a => a.Manager)
100+
.Concat(new[] { "vim", "vscode", "obsidian", "ohmyposh" }.Where(_ => config.Vim != null || config.Vscode != null || config.Obsidian != null || config.Ohmyposh != null))
101+
.Concat(config.Extensions.Keys)
102+
.Where(name => !string.IsNullOrEmpty(name))
103+
.Distinct(StringComparer.OrdinalIgnoreCase)
104+
.ToList();
105+
106+
await _pluginManager.EnsurePluginsInstalledAsync(configuredPluginNames);
107+
98108
// Load Plugins
99109
var plugins = _pluginManager.DiscoverPlugins().ToList();
100110
var loggedPlugins = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

src/Interfaces/IPluginManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ public interface IPluginManager
99
IEnumerable<PluginManifest> DiscoverPlugins();
1010
/// <summary>Ensures the required runtime is installed for the given plugin.</summary>
1111
Task EnsureRuntimeAsync(PluginManifest plugin);
12+
/// <summary>Downloads and installs missing plugins from the remote pack.</summary>
13+
Task EnsurePluginsInstalledAsync(IEnumerable<string> configuredPluginNames);
1214
}
1315
}

src/Services/Plugins/PluginManager.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,92 @@ public async Task EnsureRuntimeAsync(PluginManifest plugin)
118118
break;
119119
}
120120
}
121+
122+
/// <summary>Downloads and installs missing plugins from the remote repository archive.</summary>
123+
public async Task EnsurePluginsInstalledAsync(IEnumerable<string> configuredPluginNames)
124+
{
125+
if (!Directory.Exists(_pluginsDir))
126+
{
127+
Directory.CreateDirectory(_pluginsDir);
128+
}
129+
130+
var missingPlugins = configuredPluginNames.Where(name =>
131+
{
132+
var manifestPath = Path.Combine(_pluginsDir, name, "plugin.yaml");
133+
return !File.Exists(manifestPath);
134+
}).ToList();
135+
136+
if (!missingPlugins.Any())
137+
{
138+
return;
139+
}
140+
141+
_logger.LogInfo($"[PluginManager] Missing local plugins: {string.Join(", ", missingPlugins)}. Downloading fresh plugin pack from GitHub...");
142+
143+
try
144+
{
145+
using var client = new System.Net.Http.HttpClient();
146+
client.DefaultRequestHeaders.UserAgent.ParseAdd("WinHome-CLI");
147+
148+
var zipUrl = "https://github.com/DotDev262/WinHome/archive/refs/heads/main.zip";
149+
var tempZipPath = Path.Combine(Path.GetTempPath(), $"winhome-plugins-{Guid.NewGuid()}.zip");
150+
151+
using (var response = await client.GetAsync(zipUrl))
152+
{
153+
response.EnsureSuccessStatusCode();
154+
using (var fs = new FileStream(tempZipPath, FileMode.Create, FileAccess.Write, FileShare.None))
155+
{
156+
await response.Content.CopyToAsync(fs);
157+
}
158+
}
159+
160+
var tempExtractPath = Path.Combine(Path.GetTempPath(), $"winhome-extract-{Guid.NewGuid()}");
161+
System.IO.Compression.ZipFile.ExtractToDirectory(tempZipPath, tempExtractPath);
162+
163+
var extractedPluginsDir = Path.Combine(tempExtractPath, "WinHome-main", "plugins");
164+
if (Directory.Exists(extractedPluginsDir))
165+
{
166+
foreach (var dir in Directory.GetDirectories(extractedPluginsDir))
167+
{
168+
var pluginName = Path.GetFileName(dir);
169+
var targetDir = Path.Combine(_pluginsDir, pluginName);
170+
if (Directory.Exists(targetDir))
171+
{
172+
Directory.Delete(targetDir, true);
173+
}
174+
CopyDirectory(dir, targetDir);
175+
}
176+
_logger.LogSuccess("[PluginManager] Plugin pack downloaded and extracted successfully.");
177+
}
178+
else
179+
{
180+
_logger.LogError("[PluginManager] Failed to locate plugins folder in downloaded archive.");
181+
}
182+
183+
try
184+
{
185+
File.Delete(tempZipPath);
186+
Directory.Delete(tempExtractPath, true);
187+
}
188+
catch { /* ignored */ }
189+
}
190+
catch (Exception ex)
191+
{
192+
_logger.LogError($"[PluginManager] Failed to download plugin pack: {ex.Message}");
193+
}
194+
}
195+
196+
private static void CopyDirectory(string sourceDir, string destinationDir)
197+
{
198+
Directory.CreateDirectory(destinationDir);
199+
foreach (var file in Directory.GetFiles(sourceDir))
200+
{
201+
File.Copy(file, Path.Combine(destinationDir, Path.GetFileName(file)), true);
202+
}
203+
foreach (var subDir in Directory.GetDirectories(sourceDir))
204+
{
205+
CopyDirectory(subDir, Path.Combine(destinationDir, Path.GetFileName(subDir)));
206+
}
207+
}
121208
}
122209
}

0 commit comments

Comments
 (0)