From 94bcbc16316093c429ff71c60f6f2e22834d4544 Mon Sep 17 00:00:00 2001
From: t8y2 <1156263951@qq.com>
Date: Tue, 15 Sep 2026 17:54:32 +0800
Subject: [PATCH] feat(plugins): install marketplace packages via dbx deep link
---
apps/desktop/src/App.vue | 36 +++++++++++-
.../plugins/PluginContributionsPanel.vue | 16 ++++++
.../desktop/src/composables/useTauriEvents.ts | 12 ++++
apps/desktop/src/i18n/locales/en.ts | 2 +
apps/desktop/src/i18n/locales/es.ts | 2 +
apps/desktop/src/i18n/locales/it.ts | 2 +
apps/desktop/src/i18n/locales/ja.ts | 2 +
apps/desktop/src/i18n/locales/ko.ts | 2 +
apps/desktop/src/i18n/locales/pt-BR.ts | 2 +
apps/desktop/src/i18n/locales/zh-CN.ts | 2 +
apps/desktop/src/i18n/locales/zh-TW.ts | 2 +
apps/desktop/src/lib/backend/api.ts | 1 +
apps/desktop/src/lib/backend/http.ts | 4 ++
apps/desktop/src/lib/backend/tauri.ts | 4 ++
.../__tests__/pluginInstallDeepLink.spec.ts | 26 +++++++++
.../src/lib/plugins/pluginInstallDeepLink.ts | 31 +++++++++++
src-tauri/src/commands/deep_link.rs | 55 +++++++++++++++++++
src-tauri/src/lib.rs | 23 ++++++++
18 files changed, 223 insertions(+), 1 deletion(-)
create mode 100644 apps/desktop/src/lib/plugins/__tests__/pluginInstallDeepLink.spec.ts
create mode 100644 apps/desktop/src/lib/plugins/pluginInstallDeepLink.ts
diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue
index cae06656ca..8a2d66a997 100644
--- a/apps/desktop/src/App.vue
+++ b/apps/desktop/src/App.vue
@@ -71,6 +71,7 @@ import { rememberExternalSqlFileTarget, resolveExternalSqlFileTarget, unassociat
import { externalSqlFileOpenErrorMessage, externalSqlEditorMaxBytes, isSqlFilePath, readBrowserSqlFile, sqlFileTitleFromPath } from "@/lib/sql/sqlFileOpen";
import type { ConnectionConfig, DatabaseType, ObjectBrowserFilter, ObjectSourceKind, QueryTab, TabOutputView, TreeNode } from "@/types/database";
import type { PluginCenterFocus } from "@/lib/plugins/pluginCenterNavigation";
+import { parsePluginInstallDeepLink } from "@/lib/plugins/pluginInstallDeepLink";
import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
import { parseAiConfigDeepLink, type AiConfigDeepLinkDraft } from "@/lib/ai/aiConfigDeepLink";
import { activeDesktopAiRuns, blockingDesktopAiRunsForQuit } from "@/lib/ai/desktopAiRunRegistry";
@@ -999,6 +1000,7 @@ const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({
openConnectionDeepLink,
closeActiveSurface,
openAiConfigDeepLink,
+ openPluginInstallDeepLink,
});
const { showCloseActionPrompt, chooseQuit, chooseMinimize, cancelCloseActionPrompt, performCloseAction, setupCloseActionPromptListener, cleanupCloseActionPromptListener } = useCloseActionPrompt({ requestClose: requestAppClose });
useVisibilityChange();
@@ -2372,6 +2374,37 @@ async function openPendingAiConfigLinks() {
}
}
+const pluginCenterInstallRequest = ref<{ id: number; url: string } | null>(null);
+let pluginInstallDeepLinkId = 0;
+
+async function openPluginInstallDeepLink(url: string) {
+ try {
+ const draft = parsePluginInstallDeepLink(url);
+ if (!draft) return;
+ pluginCenterInstallRequest.value = { id: ++pluginInstallDeepLinkId, url: draft.url };
+ openPluginCenterPage();
+ } catch (e: any) {
+ toast(
+ t("pluginPlatform.deepLinkInvalid", {
+ message: e?.message || String(e),
+ }),
+ 5000,
+ );
+ }
+}
+
+async function openPendingPluginInstallLinks() {
+ if (!isTauriRuntime()) return;
+ try {
+ const links = await api.pendingOpenPluginInstallLinks();
+ for (const link of links) {
+ await openPluginInstallDeepLink(link);
+ }
+ } catch {
+ /* ignore startup deep-link probing errors */
+ }
+}
+
function setConnectionDialogOpen(value: boolean) {
showConnectionDialog.value = value;
if (!value) {
@@ -3579,6 +3612,7 @@ onMounted(async () => {
void openPendingDbFiles();
void openPendingConnectionLinks();
void openPendingAiConfigLinks();
+ void openPendingPluginInstallLinks();
console.log(`[STARTUP] onMounted sync done: ${(performance.now() - mountStart).toFixed(0)}ms`);
});
@@ -3707,7 +3741,7 @@ onUnmounted(() => {
@detach-tab="detachTab"
>
-
+
();
const emit = defineEmits<{
@@ -534,6 +535,21 @@ watch(
},
{ deep: true },
);
+let lastHandledInstallRequestId = 0;
+watch(
+ () => props.installUrlRequest,
+ async (request) => {
+ // immediate so a deep link that just opened the Plugin Center is consumed on
+ // mount; the id guard keeps already-handled requests from replaying.
+ if (!request || request.id <= lastHandledInstallRequestId) return;
+ lastHandledInstallRequestId = request.id;
+ installUrl.value = request.url;
+ if (window.confirm(t("pluginPlatform.deepLinkInstallConfirm", { url: request.url }))) {
+ await installPluginFromUrl();
+ }
+ },
+ { immediate: true },
+);
watch(allowUnsigned, (value) => {
try {
localStorage.setItem(PLUGIN_ALLOW_UNSIGNED_STORAGE_KEY, value ? "1" : "0");
diff --git a/apps/desktop/src/composables/useTauriEvents.ts b/apps/desktop/src/composables/useTauriEvents.ts
index 9f7c2b5359..55e5b83d61 100644
--- a/apps/desktop/src/composables/useTauriEvents.ts
+++ b/apps/desktop/src/composables/useTauriEvents.ts
@@ -9,6 +9,7 @@ export function useTauriEvents(deps: {
openDbFilePath: (path: string) => Promise;
openConnectionDeepLink: (url: string) => Promise;
openAiConfigDeepLink: (url: string) => Promise;
+ openPluginInstallDeepLink: (url: string) => Promise;
closeActiveSurface: () => void;
}) {
const connectionStore = useConnectionStore();
@@ -153,6 +154,17 @@ export function useTauriEvents(deps: {
}
}).then((unlisten) => unlistenHandles.push(unlisten));
+ listen("dbx-open-plugin-install-links", async (event) => {
+ try {
+ for (const url of event.payload) {
+ await deps.openPluginInstallDeepLink(url);
+ }
+ focusCurrentWindow();
+ } catch (e) {
+ console.error("[DBX] dbx-open-plugin-install-links error:", e);
+ }
+ }).then((unlisten) => unlistenHandles.push(unlisten));
+
listen("dbx-close-active-tab", () => {
deps.closeActiveSurface();
}).then((unlisten) => unlistenHandles.push(unlisten));
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index ec7ee4db08..85c0897cea 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -230,6 +230,8 @@ export default {
installPackage: "Install .dbxp",
installFromUrl: "Install from URL",
installUrlPlaceholder: "https://example.com/plugin.dbxp",
+ deepLinkInstallConfirm: "Install this plugin from the link below?",
+ deepLinkInvalid: "Invalid plugin install link: {message}",
invalidPackageUrl: "Enter a valid http:// or https:// plugin package URL",
downloadProgress: "Downloading {downloaded} of {total}",
downloadProgressUnknown: "Downloading {downloaded}",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 42e3440103..27c48089a2 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -223,6 +223,8 @@ export default withEnglishFallback({
installPackage: "Instalar .dbxp",
installFromUrl: "Instalar desde URL",
installUrlPlaceholder: "https://example.com/plugin.dbxp",
+ deepLinkInstallConfirm: "¿Instalar este complemento desde el siguiente enlace?",
+ deepLinkInvalid: "Enlace de instalación de complemento no válido: {message}",
invalidPackageUrl: "Introduce una URL de paquete de plugin http(s) válida",
downloadProgress: "Descargando {downloaded} de {total}",
downloadProgressUnknown: "Descargando {downloaded}",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index c9ee7584f6..aa8a5c3ed0 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -222,6 +222,8 @@ export default withEnglishFallback({
installPackage: "Installa .dbxp",
installFromUrl: "Installa da URL",
installUrlPlaceholder: "https://example.com/plugin.dbxp",
+ deepLinkInstallConfirm: "Installare questo plugin dal seguente link?",
+ deepLinkInvalid: "Link di installazione plugin non valido: {message}",
invalidPackageUrl: "Inserisci un URL http(s) valido per il pacchetto del plugin",
downloadProgress: "Download di {downloaded} su {total}",
downloadProgressUnknown: "Download di {downloaded}",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index 0ca2dc4db0..ee1f5130b7 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -223,6 +223,8 @@ export default withEnglishFallback({
installPackage: ".dbxp をインストール",
installFromUrl: "URL からインストール",
installUrlPlaceholder: "https://example.com/plugin.dbxp",
+ deepLinkInstallConfirm: "以下のリンクからプラグインをインストールしますか?",
+ deepLinkInvalid: "無効なプラグインインストールリンクです: {message}",
invalidPackageUrl: "有効な http(s) プラグインパッケージ URL を入力してください",
downloadProgress: "ダウンロード中 {downloaded} / {total}",
downloadProgressUnknown: "ダウンロード中 {downloaded}",
diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts
index 6ea97f7620..93eeb985ce 100644
--- a/apps/desktop/src/i18n/locales/ko.ts
+++ b/apps/desktop/src/i18n/locales/ko.ts
@@ -231,6 +231,8 @@ export default withEnglishFallback({
installPackage: ".dbxp 설치",
installFromUrl: "URL에서 설치",
installUrlPlaceholder: "https://example.com/plugin.dbxp",
+ deepLinkInstallConfirm: "아래 링크에서 플러그인을 설치하시겠습니까?",
+ deepLinkInvalid: "잘못된 플러그인 설치 링크입니다: {message}",
invalidPackageUrl: "유효한 http(s) 플러그인 패키지 URL을 입력하세요",
downloadProgress: "다운로드 중 {downloaded} / {total}",
downloadProgressUnknown: "다운로드 중 {downloaded}",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 26518cee57..7127826014 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -223,6 +223,8 @@ export default withEnglishFallback({
installPackage: "Instalar .dbxp",
installFromUrl: "Instalar de URL",
installUrlPlaceholder: "https://example.com/plugin.dbxp",
+ deepLinkInstallConfirm: "Instalar este plugin pelo link abaixo?",
+ deepLinkInvalid: "Link de instalação de plugin inválido: {message}",
invalidPackageUrl: "Insira uma URL http(s) válida do pacote do plugin",
downloadProgress: "Baixando {downloaded} de {total}",
downloadProgressUnknown: "Baixando {downloaded}",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index cb3fde243a..6f13c43f02 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -146,6 +146,8 @@ export default withEnglishFallback({
installPackage: "安装 .dbxp",
installFromUrl: "从 URL 安装",
installUrlPlaceholder: "https://example.com/plugin.dbxp",
+ deepLinkInstallConfirm: "是否从以下链接安装插件?",
+ deepLinkInvalid: "无效的插件安装链接:{message}",
invalidPackageUrl: "请输入有效的 http(s) 插件包链接",
downloadProgress: "正在下载 {downloaded} / {total}",
downloadProgressUnknown: "正在下载 {downloaded}",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index 583f3b6a43..2823cf6432 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -223,6 +223,8 @@ export default withEnglishFallback({
installPackage: "安裝 .dbxp",
installFromUrl: "從 URL 安裝",
installUrlPlaceholder: "https://example.com/plugin.dbxp",
+ deepLinkInstallConfirm: "要從以下連結安裝外掛程式嗎?",
+ deepLinkInvalid: "無效的外掛程式安裝連結:{message}",
invalidPackageUrl: "請輸入有效的 http(s) 外掛套件連結",
downloadProgress: "正在下載 {downloaded} / {total}",
downloadProgressUnknown: "正在下載 {downloaded}",
diff --git a/apps/desktop/src/lib/backend/api.ts b/apps/desktop/src/lib/backend/api.ts
index 475e5eb35a..dad796f5eb 100644
--- a/apps/desktop/src/lib/backend/api.ts
+++ b/apps/desktop/src/lib/backend/api.ts
@@ -420,6 +420,7 @@ export const pendingOpenSqlFiles = forward("pendingOpenSqlFiles");
export const pendingOpenDbFiles = forward("pendingOpenDbFiles");
export const pendingOpenConnectionLinks = forward("pendingOpenConnectionLinks");
export const pendingOpenAiConfigLinks = forward("pendingOpenAiConfigLinks");
+export const pendingOpenPluginInstallLinks = forward("pendingOpenPluginInstallLinks");
export const readExternalSqlFile = forward("readExternalSqlFile");
export const readExternalSqlFileSnapshot = forward("readExternalSqlFileSnapshot");
export const inspectExternalSqlFile = forward("inspectExternalSqlFile");
diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts
index 44eaefd0fe..df1a316646 100644
--- a/apps/desktop/src/lib/backend/http.ts
+++ b/apps/desktop/src/lib/backend/http.ts
@@ -2491,6 +2491,10 @@ export async function pendingOpenAiConfigLinks(): Promise {
return [];
}
+export async function pendingOpenPluginInstallLinks(): Promise {
+ return [];
+}
+
export async function readExternalSqlFile(_path: string, _maxSizeBytes?: number): Promise {
throw new Error("Opening external SQL file paths is only available in the desktop app");
}
diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts
index 4c5fee7b53..2a3552c4f7 100644
--- a/apps/desktop/src/lib/backend/tauri.ts
+++ b/apps/desktop/src/lib/backend/tauri.ts
@@ -1003,6 +1003,10 @@ export async function pendingOpenAiConfigLinks(): Promise {
return invoke("pending_open_ai_config_links");
}
+export async function pendingOpenPluginInstallLinks(): Promise {
+ return invoke("pending_open_plugin_install_links");
+}
+
export interface ExternalSqlFileSnapshot {
content: string;
version: ExternalSqlFileVersion;
diff --git a/apps/desktop/src/lib/plugins/__tests__/pluginInstallDeepLink.spec.ts b/apps/desktop/src/lib/plugins/__tests__/pluginInstallDeepLink.spec.ts
new file mode 100644
index 0000000000..10de4ca88a
--- /dev/null
+++ b/apps/desktop/src/lib/plugins/__tests__/pluginInstallDeepLink.spec.ts
@@ -0,0 +1,26 @@
+import { describe, expect, it } from "vitest";
+import { parsePluginInstallDeepLink } from "../pluginInstallDeepLink";
+
+const PACKAGE_URL = "https://dl.dbxio.com/plugins/io.dbx.ssh/0.4.73/io.dbx.ssh-0.4.73-darwin-arm64.dbxp";
+
+describe("parsePluginInstallDeepLink", () => {
+ it("extracts the encoded package url", () => {
+ const draft = parsePluginInstallDeepLink(`dbx://plugins/install?url=${encodeURIComponent(PACKAGE_URL)}`);
+ expect(draft).toEqual({ url: PACKAGE_URL });
+ });
+
+ it("returns null for non-plugin links and junk", () => {
+ expect(parsePluginInstallDeepLink("dbx://connection/new?type=mysql")).toBeNull();
+ expect(parsePluginInstallDeepLink("dbx://plugins/installed?url=https://example.com/a.dbxp")).toBeNull();
+ expect(parsePluginInstallDeepLink("dbx://plugins/installation?url=https://example.com/a.dbxp")).toBeNull();
+ expect(parsePluginInstallDeepLink("")).toBeNull();
+ expect(parsePluginInstallDeepLink("not a url")).toBeNull();
+ });
+
+ it("throws for matching links without a usable url param", () => {
+ expect(() => parsePluginInstallDeepLink("dbx://plugins/install")).toThrow(/Missing url/);
+ expect(() => parsePluginInstallDeepLink("dbx://plugins/install?url=")).toThrow(/Missing url/);
+ expect(() => parsePluginInstallDeepLink(`dbx://plugins/install?url=${encodeURIComponent("ftp://example.com/a.dbxp")}`)).toThrow(/http/);
+ expect(() => parsePluginInstallDeepLink(`dbx://plugins/install?url=${encodeURIComponent(`https://example.com/${"a".repeat(2100)}`)}`)).toThrow(/too long/);
+ });
+});
diff --git a/apps/desktop/src/lib/plugins/pluginInstallDeepLink.ts b/apps/desktop/src/lib/plugins/pluginInstallDeepLink.ts
new file mode 100644
index 0000000000..5fa1a15b00
--- /dev/null
+++ b/apps/desktop/src/lib/plugins/pluginInstallDeepLink.ts
@@ -0,0 +1,31 @@
+const PLUGIN_INSTALL_DEEP_LINK_HOST = "plugins";
+const PLUGIN_INSTALL_DEEP_LINK_PATH = "/install";
+const MAX_DEEP_LINK_LENGTH = 4096;
+const MAX_PACKAGE_URL_LENGTH = 2048;
+
+export interface PluginInstallDeepLinkDraft {
+ url: string;
+}
+
+/**
+ * Parses `dbx://plugins/install?url=` links.
+ * Returns null for non-matching links and throws for malformed ones,
+ * mirroring the AI config deep-link parser contract.
+ */
+export function parsePluginInstallDeepLink(value: string): PluginInstallDeepLinkDraft | null {
+ const trimmed = value.trim();
+ if (!trimmed || trimmed.length > MAX_DEEP_LINK_LENGTH) return null;
+ if (!trimmed.startsWith("dbx://")) return null;
+
+ const url = new URL(trimmed);
+ if (url.host !== PLUGIN_INSTALL_DEEP_LINK_HOST || url.pathname !== PLUGIN_INSTALL_DEEP_LINK_PATH) return null;
+
+ const packageUrl = url.searchParams.get("url")?.trim() ?? "";
+ if (!packageUrl) throw new Error("Missing url");
+ if (packageUrl.length > MAX_PACKAGE_URL_LENGTH) throw new Error("Package url is too long");
+
+ const protocol = new URL(packageUrl).protocol;
+ if (protocol !== "http:" && protocol !== "https:") throw new Error("Package url must be http(s)");
+
+ return { url: packageUrl };
+}
diff --git a/src-tauri/src/commands/deep_link.rs b/src-tauri/src/commands/deep_link.rs
index c4f388d4e8..fd632039bd 100644
--- a/src-tauri/src/commands/deep_link.rs
+++ b/src-tauri/src/commands/deep_link.rs
@@ -3,6 +3,7 @@ use std::sync::Mutex;
const CONNECTION_DEEP_LINK_PREFIX: &str = "dbx://connection/new";
const AI_CONFIG_DEEP_LINK_PREFIX: &str = "dbx://settings/ai/new";
+const PLUGIN_INSTALL_DEEP_LINK_PREFIX: &str = "dbx://plugins/install";
const APP_OPEN_DEEP_LINK_PREFIX: &str = "dbx://open";
#[tauri::command]
@@ -15,10 +16,16 @@ pub fn pending_open_ai_config_links(state: tauri::State<'_, DeepLinkOpenState>)
dedupe_links(state.drain_ai_config_links())
}
+#[tauri::command]
+pub fn pending_open_plugin_install_links(state: tauri::State<'_, DeepLinkOpenState>) -> Vec {
+ dedupe_links(state.drain_plugin_install_links())
+}
+
#[derive(Default)]
pub struct DeepLinkOpenState {
pending_connection_links: Mutex>,
pending_ai_config_links: Mutex>,
+ pending_plugin_install_links: Mutex>,
}
impl DeepLinkOpenState {
@@ -40,6 +47,15 @@ impl DeepLinkOpenState {
}
}
+ pub fn push_plugin_install_links(&self, links: Vec) {
+ if links.is_empty() {
+ return;
+ }
+ if let Ok(mut pending) = self.pending_plugin_install_links.lock() {
+ pending.extend(links);
+ }
+ }
+
fn drain_connection_links(&self) -> Vec {
self.pending_connection_links.lock().map(|mut pending| pending.drain(..).collect()).unwrap_or_default()
}
@@ -47,6 +63,10 @@ impl DeepLinkOpenState {
fn drain_ai_config_links(&self) -> Vec {
self.pending_ai_config_links.lock().map(|mut pending| pending.drain(..).collect()).unwrap_or_default()
}
+
+ fn drain_plugin_install_links(&self) -> Vec {
+ self.pending_plugin_install_links.lock().map(|mut pending| pending.drain(..).collect()).unwrap_or_default()
+ }
}
pub fn connection_deep_links_from_args(args: I) -> Vec
@@ -75,6 +95,19 @@ pub fn ai_config_deep_link_from_arg(arg: &str) -> Option {
matches_deep_link_target(trimmed, AI_CONFIG_DEEP_LINK_PREFIX).then(|| trimmed.to_string())
}
+pub fn plugin_install_deep_links_from_args(args: I) -> Vec
+where
+ I: IntoIterator- ,
+ S: AsRef,
+{
+ args.into_iter().filter_map(|arg| plugin_install_deep_link_from_arg(arg.as_ref())).collect()
+}
+
+pub fn plugin_install_deep_link_from_arg(arg: &str) -> Option {
+ let trimmed = arg.trim();
+ matches_deep_link_target(trimmed, PLUGIN_INSTALL_DEEP_LINK_PREFIX).then(|| trimmed.to_string())
+}
+
pub fn is_app_open_deep_link(arg: &str) -> bool {
matches_deep_link_target(arg.trim(), APP_OPEN_DEEP_LINK_PREFIX)
}
@@ -133,6 +166,22 @@ mod tests {
assert_eq!(links, vec!["dbx://settings/ai/new?v=1&provider=openai-compatible".to_string()]);
}
+ #[test]
+ fn filters_plugin_install_deep_links() {
+ let links = plugin_install_deep_links_from_args([
+ "dbx://plugins/install?url=https%3A%2F%2Fdl.dbxio.com%2Fplugins%2Fio.dbx.ssh%2F0.4.73%2Fio.dbx.ssh-0.4.73-darwin-arm64.dbxp",
+ "--flag",
+ "dbx://plugins/installed?url=https://example.com/plugin.dbxp",
+ "dbx://plugins/installation?url=https://example.com/plugin.dbxp",
+ "dbx://connection/new?type=mysql",
+ ]);
+
+ assert_eq!(
+ links,
+ vec!["dbx://plugins/install?url=https%3A%2F%2Fdl.dbxio.com%2Fplugins%2Fio.dbx.ssh%2F0.4.73%2Fio.dbx.ssh-0.4.73-darwin-arm64.dbxp".to_string()]
+ );
+ }
+
#[test]
fn recognizes_app_open_deep_links() {
assert!(is_app_open_deep_link("dbx://open"));
@@ -148,11 +197,17 @@ mod tests {
let state = DeepLinkOpenState::default();
state.push_connection_links(vec!["dbx://connection/new?type=mysql".to_string()]);
state.push_ai_config_links(vec!["dbx://settings/ai/new?provider=openai-compatible".to_string()]);
+ state.push_plugin_install_links(vec!["dbx://plugins/install?url=https://example.com/plugin.dbxp".to_string()]);
assert_eq!(state.drain_connection_links(), vec!["dbx://connection/new?type=mysql"]);
assert_eq!(state.drain_ai_config_links(), vec!["dbx://settings/ai/new?provider=openai-compatible"]);
+ assert_eq!(
+ state.drain_plugin_install_links(),
+ vec!["dbx://plugins/install?url=https://example.com/plugin.dbxp"]
+ );
assert!(state.drain_connection_links().is_empty());
assert!(state.drain_ai_config_links().is_empty());
+ assert!(state.drain_plugin_install_links().is_empty());
}
#[test]
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 9a31d16009..20b0d9837d 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -700,6 +700,17 @@ fn open_ai_config_deep_links(app: &tauri::AppHandle, links: Vec) {
show_main_window(app);
}
+fn open_plugin_install_deep_links(app: &tauri::AppHandle, links: Vec) {
+ if links.is_empty() {
+ return;
+ }
+ if let Some(state) = app.try_state::() {
+ state.push_plugin_install_links(links.clone());
+ }
+ let _ = app.emit("dbx-open-plugin-install-links", links);
+ show_main_window(app);
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LocaleFamily {
Azerbaijani,
@@ -1446,6 +1457,8 @@ pub fn run() {
open_connection_deep_links(app, links);
let ai_config_links = commands::deep_link::ai_config_deep_links_from_args(args.clone());
open_ai_config_deep_links(app, ai_config_links);
+ let plugin_install_links = commands::deep_link::plugin_install_deep_links_from_args(args.clone());
+ open_plugin_install_deep_links(app, plugin_install_links);
let paths = commands::external_sql::sql_file_paths_from_args(args.clone(), std::path::Path::new(&cwd));
if !paths.is_empty() {
@@ -1660,6 +1673,8 @@ pub fn run() {
open_connection_deep_links(app.handle(), startup_links);
let startup_ai_config_links = commands::deep_link::ai_config_deep_links_from_args(&startup_args);
open_ai_config_deep_links(app.handle(), startup_ai_config_links);
+ let startup_plugin_install_links = commands::deep_link::plugin_install_deep_links_from_args(&startup_args);
+ open_plugin_install_deep_links(app.handle(), startup_plugin_install_links);
let app_handle = app.handle().clone();
commands::mcp_bridge::start(app_handle, state, data_dir);
@@ -2036,6 +2051,7 @@ pub fn run() {
commands::keychain::read_keychain_passwords,
commands::deep_link::pending_open_connection_links,
commands::deep_link::pending_open_ai_config_links,
+ commands::deep_link::pending_open_plugin_install_links,
commands::table_import::preview_table_import_file,
commands::table_import::import_table_file,
commands::table_import::cancel_table_import,
@@ -2654,6 +2670,13 @@ pub fn run() {
.collect();
open_ai_config_deep_links(app_handle, ai_config_links);
+ let plugin_install_links: Vec = urls
+ .iter()
+ .map(|url| url.to_string())
+ .filter_map(|url| commands::deep_link::plugin_install_deep_link_from_arg(&url))
+ .collect();
+ open_plugin_install_deep_links(app_handle, plugin_install_links);
+
let paths: Vec = urls
.iter()
.filter_map(|url| url.to_file_path().ok())