Skip to content

Added custom plugins manifest URL option for self hosting - #4585

Open
mrmetrix4 wants to merge 1 commit into
Flow-Launcher:devfrom
mrmetrix4:dev
Open

Added custom plugins manifest URL option for self hosting#4585
mrmetrix4 wants to merge 1 commit into
Flow-Launcher:devfrom
mrmetrix4:dev

Conversation

@mrmetrix4

@mrmetrix4 mrmetrix4 commented Jul 25, 2026

Copy link
Copy Markdown

As talked in a lot of issues


Summary by cubic

Adds support for a custom plugins manifest URL for self‑hosting, with a new field in General settings. Defaults remain unchanged when the field is empty and fallbacks are still used.

Summary of changes

  • Changed: PluginsManifest.UpdateManifestAsync now takes Settings and builds CommunityPluginStore at call time, using Settings.PluginSettings.PluginsManifestUrl as primary when set, otherwise the existing default URLs. Updated PublicAPIInstance.UpdatePluginManifestAsync to pass settings. Replaced the previous static, hard-coded mainPluginStore.
  • Added: PluginSettings.PluginsManifestUrl (also mirrored to Constant.PluginsManifestURL). New General settings field to enter the URL. Added pluginManifestUrl string in en.xaml.
  • Removed: The readonly initialization of mainPluginStore with only the GitHub/CDN URLs.
  • Memory impact: Negligible. A new store instance is created on each update; the previous instance is GC‑eligible. No new caches introduced.
  • Security risks: Fetching from a user-provided URL can load untrusted manifests. Recommend using HTTPS and trusted hosts. No additional validation or sandboxing added.
  • Tests: No new unit tests. Default path verified to behave as before when the field is empty.

Release Note
You can now set a custom plugin catalog URL in Settings to use a self‑hosted plugins list.

Written for commit 46122c6. Summary will update on new commits.

Review in cubic

@github-actions github-actions Bot added this to the 2.2.0 milestone Jul 25, 2026
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Plugins manifest URL configuration

Layer / File(s) Summary
Manifest URL setting and UI
Flow.Launcher.Infrastructure/Constant.cs, Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs, Flow.Launcher/Languages/en.xaml, Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
Adds a persisted plugins manifest URL setting, localized label, shared constant, and General settings text box.
Manifest store selection
Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs
Updates manifest refreshes to receive Settings and use the configured URL or default primary URL with mirrors.
Update API wiring
Flow.Launcher/PublicAPIInstance.cs
Passes the instance settings into plugin manifest updates.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SettingsUI
  participant PublicAPIInstance
  participant PluginsManifest
  participant PluginStores
  SettingsUI->>PublicAPIInstance: set PluginsManifestUrl
  PublicAPIInstance->>PluginsManifest: UpdateManifestAsync(_settings, ...)
  PluginsManifest->>PluginStores: construct configured or default stores
  PluginsManifest->>PluginStores: fetch and update manifest
Loading

Possibly related PRs

Suggested reviewers: jjw24, victoriousraptor, jack251970

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a custom plugins manifest URL for self-hosting.
Description check ✅ Passed The description matches the changeset and explains the new self-hosted manifest URL support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs (1)

15-41: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make manifest state source-aware and synchronize store selection.

mainPluginStore is process-wide, but Lines 24-41 overwrite it from each call’s settings before acquiring manifestUpdateLock. Concurrent callers can therefore fetch through another caller’s store. Also, UserPlugins and lastFetchedAt are not keyed by customUrl, so changing the setting within the two-minute window can leave the previous manifest in use. Keep the selected store local or initialize it under the lock, and invalidate/key the cache by URL.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs` around lines 15 - 41,
Make UpdateManifestAsync source-aware by deriving the CommunityPluginStore from
the current customUrl inside the manifestUpdateLock or keeping it local, rather
than overwriting shared mainPluginStore before synchronization. Key UserPlugins
and lastFetchedAt by the effective manifest URL, and invalidate or bypass the
cached manifest when customUrl changes so each settings source fetches and uses
its own data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs`:
- Around line 26-37: Validate PluginsManifestUrl before constructing
mainPluginStore: when customUrl is non-empty, require it to be a supported
absolute HTTPS URL and handle invalid input without allowing the constructor to
escape UpdateManifestAsync’s error path. Alternatively, move the new
plugin-store construction into the existing guarded section, preserving the
default mainUrls behavior.

---

Outside diff comments:
In `@Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs`:
- Around line 15-41: Make UpdateManifestAsync source-aware by deriving the
CommunityPluginStore from the current customUrl inside the manifestUpdateLock or
keeping it local, rather than overwriting shared mainPluginStore before
synchronization. Key UserPlugins and lastFetchedAt by the effective manifest
URL, and invalidate or bypass the cached manifest when customUrl changes so each
settings source fetches and uses its own data.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d33357ed-31da-4f3e-8fa9-7a24de1f7e99

📥 Commits

Reviewing files that changed from the base of the PR and between 07a958d and 46122c6.

📒 Files selected for processing (6)
  • Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs
  • Flow.Launcher.Infrastructure/Constant.cs
  • Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs
  • Flow.Launcher/Languages/en.xaml
  • Flow.Launcher/PublicAPIInstance.cs
  • Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml

Comment on lines +26 to +37
string customUrl = settings.PluginSettings.PluginsManifestUrl;

string[] mainUrls = [
"https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/main/plugins.json",
"https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json",
"https://gcore.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json",
"https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json"
];

if (!string.IsNullOrWhiteSpace(customUrl))
{
mainPluginStore = new(customUrl, mainUrls);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant files =="
fd -i 'PluginsManifest.cs|IPluginStore|CommunityPluginStore|.*Store.*\.cs' . | sed -n '1,120p'

echo
echo "== outline PluginsManifest.cs =="
ast-grep outline Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs --view expanded || true

echo
echo "== relevant PluginsManifest.cs section =="
cat -n Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs | sed -n '1,120p'

echo
echo "== search CommunityPluginStore references =="
rg -n "class CommunityPluginStore|CommunityPluginStore|new\\(customUrl|PluginsManifestUrl|try\\s*\\{" -S . | sed -n '1,220p'

Repository: Flow-Launcher/Flow.Launcher

Length of output: 6085


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== CommunityPluginStore.cs =="
cat -n Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs | sed -n '1,240p'

echo
echo "== PluginSettings.cs section =="
cat -n Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs | sed -n '1,120p'

echo
echo "== SettingsBindingViewModel references for validation =="
rg -n "PluginsManifestUrl|plugin Store|Plugin Store|Validate|Uri\\(" Flow.Launcher/Flow.Launcher.Core -S | sed -n '1,240p'

echo
echo "== C# Uri constructor behavior probe =="
python3 - <<'PY'
# Deterministic check of inputs relative to current method ordering and comment premise.
snippet = """
            string customUrl = settings.PluginSettings.PluginsManifestUrl;
            string[] mainUrls = [...];
            if (!string.IsNullOrWhiteSpace(customUrl))
            {
                mainPluginStore = new(customUrl, mainUrls);
            } else {
                mainPluginStore = new(mainUrls[0], mainUrls[1..]);
            }
            try
            {
                await manifestUpdateLock.WaitAsync(token)...
            }
            catch (Exception e)
            {
                PublicApi.Instance.LogException(ClassName, "Http request failed", e);
            }
"""
checks = {
    "new_before_try": "new(customUrl, mainUrls)" in snippet and snippet.index("(customUrl, mainUrls)") < snippet.index("try\n            {"),
    "catch_catches_Construction": snippet.index("catch (Exception e)") > snippet.index("(customUrl, mainUrls)"),
    "operation_canceled_catches_Construction": "catch (Exception e)" in snippet.split("catch (OperationCanceledException)", 1)[1],
}
print(checks)
PY

Repository: Flow-Launcher/Flow.Launcher

Length of output: 8469


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== plugin source and URI handling =="
fd -i 'CommunityPluginSource.cs|.*Source.*\.cs' . | sed -n '1,120p'

echo
echo "== locate source files =="
rg -n "class CommunityPluginSource|record CommunityPluginSource|new Uri\\(" -S . --glob '*.cs' | sed -n '1,220p'

echo
echo "== relevant source snippets =="
for f in $(rg -l "class CommunityPluginSource|record CommunityPluginSource|new Uri\\(" . --glob '*.cs' | sed -n '1,20p'); do
  echo "--- $f"
  cat -n "$f" | sed -n '1,220p'
done

echo
echo "== UI binding validators around PluginsManifestUrl =="
rg -n "PluginsManifestUrl|ValidationRule|IValidator|NotifyDataError|Error\\(|Url|URL|Manifest" Flow.Launcher/Flow.Launcher.Core Flow.Launcher -S --glob '*.cs' | sed -n '1,260p'

echo
echo "== static behavioral probe of constructor side-effects (file shape) =="
python3 - <<'PY'
from pathlib import Path
for path in [
    Path('Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs'),
    Path('Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs'),
]:
    text = path.read_text()
    print(path)
    terms = {
        "new Uri(": text.count('new Uri('),
        "Uri": text.count("Uri"),
        "AbsoluteUri": text.count("AbsoluteUri"),
        "TryParse": text.count("TryParse"),
        "HttpWebRequest/HttpClient": any(k in text for k in ["HttpClient", "HttpWebRequest"]),
        "Constructor/Initializer side effect": ".Select(url => new CommunityPluginSource(url))" in text or "new Uri(" in text,
    }
    for k,v in terms.items():
        print(f"{k}: {v}")
PY

Repository: Flow-Launcher/Flow.Launcher

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Http.SendAsync implementation =="
cat -n Flow.Launcher.Infrastructure/Http/Http.cs | sed -n '1,230p'

echo
echo "== HttpRequestMessage(string, string) usage/tests =="
rg -n "new HttpRequestMessage\\(HttpMethod\\.Get, .*Invalid|invalid.*url|bad.*url|ManifestFileUrl|CommunityPluginSource" Flow.Launcher Flow.Launcher.Test -S --glob '*.cs' | sed -n '1,200p'

echo
echo "== focused docs check if dotnet doc XML exists =="
rg -n "class HttpRequestMessage|HttpRequestMessage\\(" . --glob '*.cs' | head -50

Repository: Flow-Launcher/Flow.Launcher

Length of output: 11762


🌐 Web query:

Microsoft documentation HttpRequestMessage constructor UriFormatException invalid URI address

💡 Result:

The HttpRequestMessage constructor in.NET does not explicitly throw a UriFormatException for an "invalid" URI string in the way commonly associated with validation errors; rather, it typically accepts a string and attempts to create a Uri object [1][2]. However, developers frequently encounter errors related to URIs when using HttpClient or HttpRequestMessage that are often confused with constructor-level validation [3][4]. Key distinctions and common causes for URI-related exceptions include: 1. InvalidOperationException: This is the most common exception encountered when using HttpClient, often accompanied by the message: "An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set" [3][4][5]. This occurs during the request execution process (e.g., SendAsync) when the HttpClient cannot resolve a full, absolute URI because the provided RequestUri is relative and the HttpClient.BaseAddress is null [4][6]. 2. UriFormatException: This exception is thrown by the System.Uri constructor if the string provided is malformed or cannot be parsed into a valid URI format [7]. If you manually create a Uri object using new Uri(string) before passing it to the HttpRequestMessage constructor, the Uri constructor will throw this exception if the input string is invalid [8][7]. 3. Common Resolution Strategies: - Ensure the URI is absolute: If you are not using a BaseAddress, provide a full, absolute URI (e.g., https://example.com/api) [3]. - Configure BaseAddress: If using relative paths, ensure the HttpClient instance has its BaseAddress property correctly set [4][6]. - Use UriKind: When manually creating Uri objects for relative paths, explicitly specify UriKind.Relative to avoid parsing errors [8]. In summary, if you are receiving an exception related to an "invalid URI" when using HttpRequestMessage, it is usually an InvalidOperationException indicating a missing base address or an incorrectly formatted string passed to the Uri constructor [4]. Always verify that your URI strings are properly formatted and that the HttpClient state aligns with your chosen URI structure [3][6].

Citations:


Validate PluginsManifestUrl before creating the plugin store.

UpdateManifestAsync passes the editable value directly to new(customUrl, mainUrls), while the method’s try/catch starts after that construction. Accept only supported absolute https URLs before this path, or move/create the store inside the guarded section; otherwise malformed URL input can fail before FetchAsync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs` around lines 26 - 37,
Validate PluginsManifestUrl before constructing mainPluginStore: when customUrl
is non-empty, require it to be a supported absolute HTTPS URL and handle invalid
input without allowing the constructor to escape UpdateManifestAsync’s error
path. Alternatively, move the new plugin-store construction into the existing
guarded section, preserving the default mainUrls behavior.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs">

<violation number="1" location="Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs:26">
P2: Concurrent manifest updates can replace the shared `mainPluginStore` before the lock is acquired, so one request may fetch using another request's manifest URL. Build or select the store under `manifestUpdateLock` (or use a local immutable store) so each update uses a consistent URL configuration.</violation>

<violation number="2" location="Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs:37">
P1: A custom manifest is not reliably used: built-in manifests race the configured URL and usually win, so self-hosted plugins can be silently replaced by the public manifest. Create the custom store without these unrelated secondary URLs.</violation>

<violation number="3" location="Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs:39">
P3: Scheduled and manual refreshes now redownload the full manifest instead of using the existing conditional-request cache, because each call discards `CommunityPluginSource.latestEtag`. Retain the store while its configured URL is unchanged, and recreate/reset it only when that configuration changes.</violation>
</file>

<file name="Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml">

<violation number="1" location="Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml:512">
P2: The custom URL setting is not represented by the added `Constant.PluginsManifestURL` field at runtime: manifest loading reads `Settings.PluginSettings.PluginsManifestUrl` directly, and repository references show no consumer of `Constant.PluginsManifestURL`. Remove the unused global assignment/field or route the manifest-loading code through one consistent source to avoid a misleading configuration path.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


if (!string.IsNullOrWhiteSpace(customUrl))
{
mainPluginStore = new(customUrl, mainUrls);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A custom manifest is not reliably used: built-in manifests race the configured URL and usually win, so self-hosted plugins can be silently replaced by the public manifest. Create the custom store without these unrelated secondary URLs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs, line 37:

<comment>A custom manifest is not reliably used: built-in manifests race the configured URL and usually win, so self-hosted plugins can be silently replaced by the public manifest. Create the custom store without these unrelated secondary URLs.</comment>

<file context>
@@ -24,8 +21,24 @@ public static class PluginsManifest
+
+            if (!string.IsNullOrWhiteSpace(customUrl))
+            {
+                mainPluginStore = new(customUrl, mainUrls);
+            } else {
+                mainPluginStore = new(mainUrls[0], mainUrls[1..]);
</file context>
Suggested change
mainPluginStore = new(customUrl, mainUrls);
mainPluginStore = new(customUrl);

<TextBox
Width="370"
Height="34"
Text="{Binding Settings.PluginSettings.PluginsManifestUrl,TargetNullValue='None'}" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The custom URL setting is not represented by the added Constant.PluginsManifestURL field at runtime: manifest loading reads Settings.PluginSettings.PluginsManifestUrl directly, and repository references show no consumer of Constant.PluginsManifestURL. Remove the unused global assignment/field or route the manifest-loading code through one consistent source to avoid a misleading configuration path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml, line 512:

<comment>The custom URL setting is not represented by the added `Constant.PluginsManifestURL` field at runtime: manifest loading reads `Settings.PluginSettings.PluginsManifestUrl` directly, and repository references show no consumer of `Constant.PluginsManifestURL`. Remove the unused global assignment/field or route the manifest-loading code through one consistent source to avoid a misleading configuration path.</comment>

<file context>
@@ -504,6 +504,15 @@
+                    <TextBox
+                        Width="370"
+                        Height="34"
+                        Text="{Binding Settings.PluginSettings.PluginsManifestUrl,TargetNullValue='None'}" />
+                </StackPanel>
+            </ui:SettingsCard>
</file context>

public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default)
public static async Task<bool> UpdateManifestAsync(Settings settings, bool usePrimaryUrlOnly = false, CancellationToken token = default)
{
string customUrl = settings.PluginSettings.PluginsManifestUrl;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Concurrent manifest updates can replace the shared mainPluginStore before the lock is acquired, so one request may fetch using another request's manifest URL. Build or select the store under manifestUpdateLock (or use a local immutable store) so each update uses a consistent URL configuration.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs, line 26:

<comment>Concurrent manifest updates can replace the shared `mainPluginStore` before the lock is acquired, so one request may fetch using another request's manifest URL. Build or select the store under `manifestUpdateLock` (or use a local immutable store) so each update uses a consistent URL configuration.</comment>

<file context>
@@ -24,8 +21,24 @@ public static class PluginsManifest
-        public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default)
+        public static async Task<bool> UpdateManifestAsync(Settings settings, bool usePrimaryUrlOnly = false, CancellationToken token = default)
         {
+            string customUrl = settings.PluginSettings.PluginsManifestUrl;
+
+            string[] mainUrls = [
</file context>

{
mainPluginStore = new(customUrl, mainUrls);
} else {
mainPluginStore = new(mainUrls[0], mainUrls[1..]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Scheduled and manual refreshes now redownload the full manifest instead of using the existing conditional-request cache, because each call discards CommunityPluginSource.latestEtag. Retain the store while its configured URL is unchanged, and recreate/reset it only when that configuration changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs, line 39:

<comment>Scheduled and manual refreshes now redownload the full manifest instead of using the existing conditional-request cache, because each call discards `CommunityPluginSource.latestEtag`. Retain the store while its configured URL is unchanged, and recreate/reset it only when that configuration changes.</comment>

<file context>
@@ -24,8 +21,24 @@ public static class PluginsManifest
+            {
+                mainPluginStore = new(customUrl, mainUrls);
+            } else {
+                mainPluginStore = new(mainUrls[0], mainUrls[1..]);
+            }
+
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant